Aprenda o OpenGL. Lição 6.4 - IBL. Exposição especular

OGL3
Na lição anterior, preparamos nosso modelo de PBR para trabalhar com o método IBL - para isso, precisamos preparar um mapa de irradiação antecipadamente que descreva a parte difusa da iluminação indireta. Nesta lição, prestaremos atenção à segunda parte da expressão da refletividade - o espelho:

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




Você pode notar que o componente espelho Cook-Torrens (subexpressão com um fator ks ) não é constante e depende da direção da luz incidente, bem como da direção da observação. A solução dessa integral para todas as direções possíveis de incidência da luz, juntamente com todas as direções possíveis da observação em tempo real, simplesmente não é viável. Portanto, os pesquisadores da Epic Games propuseram uma abordagem chamada aproximação por soma dividida , que permite preparar parcialmente os dados para o componente espelho antecipadamente, sujeito a determinadas condições.

Nesta abordagem, o componente espelho da expressão da refletividade é dividido em duas partes, que podem ser pré-convoluídas separadamente e depois combinadas em um sombreador PBR, para uso como fonte de radiação especular indireta. Como na geração do mapa de irradiação, o processo de convolução recebe um mapa do ambiente HDR em sua entrada.

Para entender o método de aproximação de soma dividida, vejamos novamente a expressão da refletividade, deixando apenas a subexpressão para o componente espelho (a parte difusa foi considerada separadamente na lição anterior ):

Lo(p, omegao)= int limits Omega(ks fracDFG4( omegao cdotn)( omegai cdotn)Li(p, omegai)n cdot omegaid omegai= int limit Omegafr(p, omegai, omegao)Li(p, omegai)n cdot omegaid omegai


Assim como na preparação do mapa de irradiação, essa integral não é possível resolver em tempo real. Portanto, é desejável calcular de maneira semelhante o mapa para o componente espelho da expressão de refletividade e, no ciclo principal de renderização, faça uma seleção simples desse mapa com base no normal para a superfície. No entanto, nem tudo é tão simples: o mapa de irradiação foi obtido com relativa facilidade devido ao fato de a integral depender apenas de  omegai , e a subexpressão constante do componente difuso lambertiano pode ser retirada do sinal da integral. Nesse caso, a integral depende não apenas da  omegai isso é fácil de entender a partir da fórmula BRDF:

fr(p,wi,wo)= fracDFG4( omegao cdotn)( omegai cdotn)


A expressão sob a integral também depende de  omegao - para dois vetores de direção, é quase impossível selecionar um mapa cúbico previamente preparado. Posição do ponto p Nesse caso, você não pode levar em consideração - por que isso foi discutido na lição anterior. Cálculo preliminar da integral para todas as combinações possíveis  omegai e  omegao impossível em tarefas em tempo real.

O método de quantia dividida da Epic Games resolve esse problema dividindo o problema de cálculo preliminar em duas partes independentes, cujos resultados podem ser combinados posteriormente para obter o valor final calculado. O método de soma dividida extrai duas integrais da expressão original para o componente de espelho:

Lo(p, omegao)= int limits OmegaLi(p, omegai)d omegai int limit Omegafr(p, omegai, omegao)n cdot omegaid omegai


O resultado do cálculo da primeira parte é geralmente chamado de mapa do ambiente pré-filtrado ( mapa do ambiente pré-filtrado ) e é um mapa do ambiente, sujeito ao processo de convolução especificado por esta expressão. Tudo isso é semelhante ao processo de obtenção de um mapa de irradiação, mas, neste caso, a convolução é realizada considerando o valor da rugosidade. Valores de rugosidade mais altos levam ao uso de vetores de amostragem mais díspares no processo de convolução, o que resulta em resultados mais desfocados. O resultado da convolução para cada próximo nível de rugosidade selecionado é armazenado no próximo nível mip do mapa de ambiente preparado. Por exemplo, um mapa de ambiente, convolvido para cinco níveis diferentes de rugosidade, contém cinco níveis mip e se parece com isso:


Os vetores de amostra e sua dispersão são determinados com base na função de distribuição normal ( NDF ) do modelo BRDF de Cook-Torrens. Esta função aceita o vetor normal e a direção da observação como parâmetros de entrada. Como a direção da observação não é conhecida com antecedência no momento do cálculo preliminar, os desenvolvedores da Epic Games tiveram que fazer mais uma suposição: a direção do olhar (e, portanto, a direção da reflexão especular) é sempre idêntica à direção de saída da amostra  omegao . Sob a forma de código:

vec3 N = normalize(w_o); vec3 R = N; vec3 V = R; 

Nessas condições, a direção do olhar não é necessária no processo de convolução do mapa do ambiente, o que viabiliza o cálculo em tempo real. Mas, por outro lado, perdemos a distorção característica das reflexões especulares quando observadas em um ângulo agudo em relação à superfície reflexiva, como pode ser visto na imagem abaixo (de Moving Frostbite para PBR ). Em geral, esse compromisso é considerado aceitável.


A segunda parte da expressão de soma dividida contém o BRDF da expressão original para o componente de espelho. Supondo que o brilho da energia recebida seja representado espectralmente por luz branca em todas as direções (ou seja, L(p,x)=1,0 ), é possível pré-calcular o valor para BRDF com os seguintes parâmetros de entrada: rugosidade do material e o ângulo entre o normal n e direção da luz  omegai (ou n cdot omegai ) A abordagem da Epic Games envolve armazenar os resultados do cálculo do BRDF para cada combinação de rugosidade e o ângulo entre o normal e a direção da luz na forma de uma textura bidimensional conhecida como mapa de integração do BRDF , que mais tarde é usada como uma tabela de pesquisa ( LUT ) . Essa textura de referência usa os canais de saída vermelho e verde para armazenar a escala e o deslocamento para calcular o coeficiente de Fresnel da superfície, o que finalmente nos permite resolver a segunda parte da expressão para a soma separada:


Essa textura auxiliar é criada da seguinte maneira: coordenadas de textura horizontal (variando de [0., 1.]) são consideradas como valores de parâmetro de entrada n cdot omegai Funções BRDF; as coordenadas verticais da textura são consideradas como valores de rugosidade de entrada.

Como resultado, tendo um mapa de integração e um mapa de ambiente pré-processado, é possível combinar as amostras deles para obter o valor final da expressão integral do componente de espelho:

 float lod = getMipLevelFromRoughness(roughness); vec3 prefilteredColor = textureCubeLod(PrefilteredEnvMap, refVec, lod); vec2 envBRDF = texture2D(BRDFIntegrationMap, vec2(NdotV, roughness)).xy; vec3 indirectSpecular = prefilteredColor * (F * envBRDF.x + envBRDF.y) 

Essa revisão do método de soma dividida da Epic Games deve ajudar a fornecer uma impressão do processo de aproximação da parte da expressão de refletância responsável pelo componente de espelho. Agora vamos tentar preparar os dados do cartão.

Pré-filtrando o mapa do ambiente HDR


A pré-filtragem do mapa do ambiente é semelhante ao que foi feito para obter um mapa de irradiação. A única diferença é que agora levamos em conta a rugosidade e salvamos o resultado para cada nível de rugosidade no novo nível mip do mapa cúbico.

Primeiro, você precisará criar um novo mapa cúbico que conterá o resultado da pré-filtragem. Para criar o número necessário de níveis mip, simplesmente chamamos glGenerateMipmaps () - a memória necessária será alocada para a textura atual:

 unsigned int prefilterMap; glGenTextures(1, &prefilterMap); glBindTexture(GL_TEXTURE_CUBE_MAP, prefilterMap); for (unsigned int i = 0; i < 6; ++i) { glTexImage2D(GL_TEXTURE_CUBE_MAP_POSITIVE_X + i, 0, GL_RGB16F, 128, 128, 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_MIPMAP_LINEAR); glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MAG_FILTER, GL_LINEAR); glGenerateMipmap(GL_TEXTURE_CUBE_MAP); 

Observe: como a seleção do prefilterMap será baseada na existência de níveis mip, é necessário definir o modo de filtro de redução como GL_LINEAR_MIPMAP_LINEAR para ativar a filtragem trilinear. Imagens pré-processadas de imagens em espelho são armazenadas em faces separadas do mapa cúbico com uma resolução no nível mip básico de apenas 128x128 pixels. Para a maioria dos materiais, isso é suficiente, no entanto, se sua cena tiver um número maior de superfícies lisas e brilhantes (por exemplo, um carro novo), talvez seja necessário aumentar esta resolução.

Na lição anterior, desenvolvemos o mapa do ambiente criando vetores de amostra que são distribuídos igualmente no hemisfério  Omega usando coordenadas esféricas. Para obter irradiação, esse método é bastante eficaz, o que não pode ser dito sobre os cálculos de reflexões especulares. A física dos destaques especulares nos diz que a direção da luz refletida especularmente é adjacente ao vetor de reflexão r para uma superfície normal n mesmo se a rugosidade não for zero:


A forma generalizada de possíveis direções de saída da reflexão é chamada de lobo especular ( lóbulo especular ; "pétala de um padrão de radiação espelhada" - talvez muito detalhada, aprox. Per. ). Com o aumento da rugosidade, a pétala cresce e se expande. Além disso, sua forma muda dependendo da direção da incidência de luz. Assim, a forma da pétala é altamente dependente das propriedades do material.

Voltando ao modelo de micro-superfícies, podemos imaginar a forma do lóbulo do espelho como descrevendo a orientação da reflexão em relação ao vetor mediano das micro-superfícies, levando em consideração alguma direção dada à incidência da luz. Entendendo que a maioria dos raios de luz refletida está dentro de uma pétala de espelho orientada com base no vetor mediano, faz sentido criar vetores de amostra orientados de maneira semelhante. Caso contrário, muitos deles serão inúteis. Essa abordagem é chamada de amostragem importante .

Integração Monte Carlo e amostragem de significância


Para entender completamente o significado da amostra em termos de significância, você primeiro terá que se familiarizar com um aparato matemático como o método de integração de Monte Carlo. Este método é baseado em uma combinação de estatística e teoria das probabilidades e ajuda a resolver numericamente um problema estatístico em uma amostra grande, sem a necessidade de considerar cada elemento dessa amostra.

Por exemplo, você deseja calcular o crescimento médio da população de um país. Para obter um resultado preciso e confiável, seria necessário medir o crescimento de cada cidadão e calcular a média do resultado. No entanto, como a população da maioria dos países é bastante grande, essa abordagem é praticamente irrealizável, pois requer muitos recursos para execução.
Outra abordagem é criar uma subamostra menor preenchida com elementos verdadeiramente aleatórios (imparciais) da amostra original. Em seguida, você também mede o crescimento e calcula a média do resultado dessa subamostra. Você pode pegar pelo menos cem pessoas e obter um resultado, embora não seja absolutamente preciso, mas ainda bastante próximo da situação real. A explicação para esse método está na consideração da lei de grandes números. E sua essência é descrita desta maneira: o resultado de algumas medições em uma subamostra de tamanho menor N , composto por elementos verdadeiramente aleatórios do conjunto original, estará próximo do resultado do controle das medições realizadas em todo o conjunto inicial. Além disso, o resultado aproximado tende a ser verdadeiro com o crescimento N .
A integração de Monte Carlo é a aplicação da lei de grandes números para resolver integrais. Em vez de resolver a integral, levando em consideração todo o conjunto (possivelmente infinito) de valores x nós usamos N pontos de amostra aleatórios e calcule a média do resultado. Com crescimento N o resultado aproximado é garantido para chegar perto da solução exata da integral.

O= int limitsbaf(x)dx= frac1N sumN1i=0 fracf(x)pdf(x)


Para resolver a integral, obtemos o valor do integrando para N pontos aleatórios da amostra em [a, b], os resultados são resumidos e divididos pelo número total de pontos obtidos para a média. Item pdf descreve a função de densidade de probabilidade , que mostra a probabilidade com que cada valor selecionado ocorre na amostra original. Por exemplo, esta função para o crescimento dos cidadãos seria algo como isto:


Pode-se observar que, ao usar pontos de amostragem aleatórios, temos uma chance muito maior de atingir um valor de crescimento de 170 cm do que alguém com um crescimento de 150 cm.

É claro que durante a integração de Monte Carlo, alguns pontos de amostra são mais prováveis ​​de aparecer na sequência do que outros. Portanto, em qualquer expressão para a estimativa de Monte Carlo, dividimos ou multiplicamos o valor selecionado pela probabilidade de sua ocorrência usando a função de densidade de probabilidade. No momento, ao avaliar a integral, criamos muitos pontos de amostra distribuídos uniformemente: a chance de obter qualquer um deles era a mesma. Assim, nossa estimativa foi imparcial , o que significa que, à medida que o número de pontos de amostra aumenta, nossa estimativa converge para a solução exata da integral.

No entanto, existem funções de avaliação tendenciosas , ou seja, implicando a criação de pontos de amostragem não de maneira verdadeiramente aleatória, mas com predominância de alguma magnitude ou direção. Tais funções de avaliação permitem que a estimativa de Monte Carlo converja para a solução exata muito mais rapidamente . Por outro lado, devido ao viés da função de avaliação, a solução pode nunca convergir. No caso geral, isso é considerado um compromisso aceitável, especialmente em problemas de computação gráfica, uma vez que a estimativa é muito próxima do resultado analítico e não é necessária se seu efeito parecer visualmente bastante confiável. Como veremos em breve a amostra por significância (usar uma função de estimativa tendenciosa) permite criar pontos de amostra inclinados em direção a uma determinada direção, que é levada em consideração multiplicando ou dividindo cada valor selecionado pelo valor correspondente da função de densidade de probabilidade.

A integração de Monte Carlo é bastante comum em problemas de computação gráfica, pois é um método bastante intuitivo para estimar o valor de integrais contínuas por um método numérico, o que é bastante eficaz. Basta ter alguma área ou volume em que a amostra está sendo coletada (por exemplo, nosso hemisfério  Omega ), crie N pontos de amostragem aleatórios localizados no interior e realizar um somatório ponderado dos valores obtidos.

O método Monte Carlo é um tópico muito extenso de discussão, e aqui não vamos mais entrar em detalhes, mas há mais um detalhe importante: não existe uma maneira de criar amostras aleatórias . Por padrão, cada ponto de amostra é completamente aleatório (psvedo) - que é o que esperamos. Mas, usando certas propriedades de sequências quase aleatórias, é possível criar conjuntos de vetores que, embora aleatórios, têm propriedades interessantes. Por exemplo, ao criar amostras aleatórias para o processo de integração, você pode usar as chamadas sequências de baixa discrepância , que garantem a aleatoriedade dos pontos de amostragem criados, mas no conjunto geral eles são distribuídos de maneira mais uniforme:


O uso de sequências de baixa incompatibilidade para criar um conjunto de vetores de amostra para o processo de integração é o método de integração de Quasi-Monte Carlo . Os quase-métodos de Monte Carlo convergem muito mais rapidamente que a abordagem geral, que é uma propriedade muito atraente para aplicativos com requisitos de alto desempenho.

Portanto, conhecemos o método geral e quase-Monte Carlo, mas há mais um detalhe que fornecerá uma taxa de convergência ainda maior: uma amostra por significância.
Como já observado na lição, para reflexões especulares, a direção da luz refletida é encerrada em um lóbulo especular, cujo tamanho e formato dependem da rugosidade da superfície refletora. Entendendo que quaisquer vetores de amostra aleatórios (quase) que estão fora do lóbulo do espelho não afetarão a expressão integral do componente do espelho, ou seja, inútil. Faz sentido concentrar a geração de vetores de amostra na região do lóbulo do espelho usando a função de estimativa tendenciosa para o método de Monte Carlo.

Essa é a essência da amostragem por significado: a criação de vetores de amostragem é encerrada em uma determinada área orientada ao longo do vetor mediano das micro-superfícies, cuja forma é determinada pela rugosidade do material. Utilizando uma combinação do quase-método de Monte Carlo, sequências de baixa incompatibilidade e viés no processo de criação de vetores de amostra devido à amostragem significativa, alcançamos taxas de convergência muito altas. Como a convergência para a solução é rápida o suficiente, podemos usar um número menor de vetores de amostra para obter uma estimativa aceitável o suficiente. A combinação de métodos descrita, em princípio, permite que aplicações gráficas resolvam até a integral do componente espelho em tempo real, embora o cálculo preliminar ainda permaneça uma abordagem muito mais lucrativa.

Sequência de baixa incompatibilidade


Nesta lição, ainda usamos um cálculo preliminar do componente espelho da expressão de refletância para radiação indireta. E usaremos uma amostra de significância usando uma sequência aleatória de baixa incompatibilidade e o quase-método de Monte Carlo. A sequência usada é conhecida como sequência de Hammersley , cuja descrição detalhada é dada por Holger Dammertz . Essa sequência, por sua vez, é baseada na sequência de van der Corput , que usa uma transformação binária especial da fração decimal em relação ao ponto decimal.

Usando truques aritméticos bit a bit, você pode definir com eficiência a sequência de van der Corpute diretamente no shader e, com base nela, criar o i-ésimo elemento da sequência de Hammersley a partir da seleção em N itens:

 float RadicalInverse_VdC(uint bits) { bits = (bits << 16u) | (bits >> 16u); bits = ((bits & 0x55555555u) << 1u) | ((bits & 0xAAAAAAAAu) >> 1u); bits = ((bits & 0x33333333u) << 2u) | ((bits & 0xCCCCCCCCu) >> 2u); bits = ((bits & 0x0F0F0F0Fu) << 4u) | ((bits & 0xF0F0F0F0u) >> 4u); bits = ((bits & 0x00FF00FFu) << 8u) | ((bits & 0xFF00FF00u) >> 8u); return float(bits) * 2.3283064365386963e-10; // / 0x100000000 } // ---------------------------------------------------------------------------- vec2 Hammersley(uint i, uint N) { return vec2(float(i)/float(N), RadicalInverse_VdC(i)); } 

Hammersley () retorna o i-ésimo elemento de uma sequência de baixa incompatibilidade de amostras de vários tamanhos N .
Nem todos os drivers OpenGL suportam operações bit a bit (WebGL e OpenGL ES 2.0, por exemplo); portanto, em certos ambientes, uma implementação alternativa de seu uso pode ser necessária:

 float VanDerCorpus(uint n, uint base) { float invBase = 1.0 / float(base); float denom = 1.0; float result = 0.0; for(uint i = 0u; i < 32u; ++i) { if(n > 0u) { denom = mod(float(n), 2.0); result += denom * invBase; invBase = invBase / 2.0; n = uint(float(n) / 2.0); } } return result; } // ---------------------------------------------------------------------------- vec2 HammersleyNoBitOps(uint i, uint N) { return vec2(float(i)/float(N), VanDerCorpus(i, 2u)); } 

Observo que, devido a certas restrições sobre os operadores de ciclo no hardware antigo, essa implementação passa por todos os 32 bits. Como resultado, esta versão não é tão produtiva quanto a primeira opção - mas funciona em qualquer hardware e até na ausência de operações de bit.

Amostra de importância no modelo GGX


Em vez de uma distribuição uniforme ou aleatória (Monte Carlo) dos vetores de amostra gerados no hemisfério  Omega , que aparece na integral que estamos resolvendo, tentaremos criar vetores para que eles gravitem na direção principal da reflexão da luz, caracterizada pelo vetor mediano das micro-superfícies e dependendo da rugosidade da superfície. O processo de amostragem em si será semelhante ao considerado anteriormente: abra um ciclo com um número suficientemente grande de iterações, crie um elemento de uma sequência de baixa incompatibilidade, com base nele, criamos um vetor de amostragem no espaço tangente, transferimos esse vetor para coordenadas mundiais e usamos o brilho de energia da cena para amostrar. Em princípio, as alterações estão relacionadas apenas ao fato de que agora um elemento da sequência de baixa incompatibilidade é usado para especificar um novo vetor de amostra:

 const uint SAMPLE_COUNT = 4096u; for(uint i = 0u; i < SAMPLE_COUNT; ++i) { vec2 Xi = Hammersley(i, SAMPLE_COUNT); 

Além disso, para a formação completa do vetor de amostra, será necessário orientá-lo de alguma forma na direção do lóbulo do espelho correspondente a um determinado nível de rugosidade. Você pode tirar o NDF (função de distribuição normal) de uma lição de teoria e combinar com o GGX NDF para o método de especificar um vetor de amostra em uma esfera de autoria da Epic Games:

 vec3 ImportanceSampleGGX(vec2 Xi, vec3 N, float roughness) { float a = roughness*roughness; float phi = 2.0 * PI * Xi.x; float cosTheta = sqrt((1.0 - Xi.y) / (1.0 + (a*a - 1.0) * Xi.y)); float sinTheta = sqrt(1.0 - cosTheta*cosTheta); //       vec3 H; Hx = cos(phi) * sinTheta; Hy = sin(phi) * sinTheta; Hz = cosTheta; //        vec3 up = abs(Nz) < 0.999 ? vec3(0.0, 0.0, 1.0) : vec3(1.0, 0.0, 0.0); vec3 tangent = normalize(cross(up, N)); vec3 bitangent = cross(N, tangent); vec3 sampleVec = tangent * Hx + bitangent * Hy + N * Hz; return normalize(sampleVec); } 

O resultado é um vetor de amostra, aproximadamente orientado ao longo do vetor mediano das micro-superfícies, para uma dada rugosidade e um elemento da sequência de baixa incompatibilidade Xi . Observe que a Epic Games usa o quadrado do valor da rugosidade para obter maior qualidade visual, com base no trabalho original da Disney no método PBR.

Após concluir a implementação da sequência de Hammersley e o código de geração de vetor de amostra, podemos fornecer o código de sombreamento de pré-filtragem e convolução:

 #version 330 core out vec4 FragColor; in vec3 localPos; uniform samplerCube environmentMap; uniform float roughness; const float PI = 3.14159265359; float RadicalInverse_VdC(uint bits); vec2 Hammersley(uint i, uint N); vec3 ImportanceSampleGGX(vec2 Xi, vec3 N, float roughness); void main() { vec3 N = normalize(localPos); vec3 R = N; vec3 V = R; const uint SAMPLE_COUNT = 1024u; float totalWeight = 0.0; vec3 prefilteredColor = vec3(0.0); for(uint i = 0u; i < SAMPLE_COUNT; ++i) { vec2 Xi = Hammersley(i, SAMPLE_COUNT); vec3 H = ImportanceSampleGGX(Xi, N, roughness); vec3 L = normalize(2.0 * dot(V, H) * H - V); float NdotL = max(dot(N, L), 0.0); if(NdotL > 0.0) { prefilteredColor += texture(environmentMap, L).rgb * NdotL; totalWeight += NdotL; } } prefilteredColor = prefilteredColor / totalWeight; FragColor = vec4(prefilteredColor, 1.0); } 


Realizamos a filtragem preliminar do mapa do ambiente com base em uma determinada rugosidade, cujo nível muda para cada nível mip do mapa cúbico resultante (de 0,0 a 1,0), e o resultado do filtro é armazenado na variável pré- filtradaColor . Em seguida, a variável é dividida pelo peso total de toda a amostra, e as amostras com menor contribuição para o resultado final (com um valor NdotL menor ) também aumentam menos o peso total.

Salvando dados de pré-filtragem em níveis mip


Resta escrever um código que instrua diretamente o OpenGL a filtrar o mapa do ambiente com vários níveis de rugosidade e salvar os resultados em uma série de níveis mip do mapa cúbico de destino. Aqui, o código já preparado da lição sobre o cálculo do mapa de irradiação é útil :

 prefilterShader.use(); prefilterShader.setInt("environmentMap", 0); prefilterShader.setMat4("projection", captureProjection); glActiveTexture(GL_TEXTURE0); glBindTexture(GL_TEXTURE_CUBE_MAP, envCubemap); glBindFramebuffer(GL_FRAMEBUFFER, captureFBO); unsigned int maxMipLevels = 5; for (unsigned int mip = 0; mip < maxMipLevels; ++mip) { //        - unsigned int mipWidth = 128 * std::pow(0.5, mip); unsigned int mipHeight = 128 * std::pow(0.5, mip); glBindRenderbuffer(GL_RENDERBUFFER, captureRBO); glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH_COMPONENT24, mipWidth, mipHeight); glViewport(0, 0, mipWidth, mipHeight); float roughness = (float)mip / (float)(maxMipLevels - 1); prefilterShader.setFloat("roughness", roughness); for (unsigned int i = 0; i < 6; ++i) { prefilterShader.setMat4("view", captureViews[i]); glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_CUBE_MAP_POSITIVE_X + i, prefilterMap, mip); glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); renderCube(); } } glBindFramebuffer(GL_FRAMEBUFFER, 0); 

O processo é semelhante a uma convolução do mapa de irradiação, mas desta vez você deve especificar o tamanho do buffer do quadro em cada etapa, reduzindo-o pela metade para corresponder aos níveis de mip. Além disso, o nível mip no qual a renderização será executada no momento deve ser especificado como um parâmetro para a função glFramebufferTexture2D () .

O resultado da execução desse código deve ser um mapa cúbico contendo imagens cada vez mais borradas de reflexões em cada nível de mip subsequente. Você pode usar um mapa cúbico como fonte de dados para o skybox e coletar uma amostra de qualquer nível mip abaixo de zero:

 vec3 envColor = textureLod(environmentMap, WorldPos, 1.2).rgb; 

O resultado desta ação será a seguinte imagem:


Parece um mapa do ambiente de origem muito desfocado. Se o resultado for semelhante, o mais provável é que o processo de filtragem preliminar do mapa do ambiente HDR seja executado corretamente. Tente experimentar uma amostra de diferentes níveis de mip e observe um aumento gradual no desfoque a cada próximo nível.

Artefatos de convolução pré-filtro


Para a maioria das tarefas, a abordagem descrita funciona muito bem, mas mais cedo ou mais tarde você terá que encontrar vários artefatos que o processo de pré-filtragem gera. Aqui estão os métodos mais comuns de lidar com eles.

A manifestação das costuras do mapa cúbico


A seleção de valores do mapa em cubos processados ​​pelo filtro preliminar para superfícies com alta rugosidade leva à leitura de dados do nível mip em algum lugar mais próximo do final de sua cadeia. Ao amostrar a partir de um mapa cúbico, o OpenGL, por padrão, não interpola linearmente entre as faces do mapa cúbico. Como os altos níveis de mip têm resolução mais baixa e o mapa do ambiente foi convocado levando em consideração um lóbulo do espelho muito maior, a ausência de filtragem de textura entre as faces se torna óbvia:


Felizmente, o OpenGL tem a capacidade de ativar essa filtragem com um simples sinalizador:

 glEnable(GL_TEXTURE_CUBE_MAP_SEAMLESS); 

É suficiente definir o sinalizador em algum lugar no código de inicialização do aplicativo e esse artefato é eliminado.

Aparecem pontos brilhantes


Como as reflexões espelhadas no caso geral contêm detalhes de alta frequência, bem como regiões com brilho muito diferente, sua convolução requer o uso de um grande número de pontos de amostra para levar em conta corretamente a grande difusão de valores dentro das reflexões HDR do ambiente. No exemplo, já coletamos um número bastante grande de amostras, mas para certas cenas e altos níveis de rugosidade do material isso ainda não será suficiente, e você testemunhará o aparecimento de muitos pontos em torno de áreas claras:


Você pode aumentar ainda mais o número de amostras, mas isso não será uma solução universal e, em algumas condições, ainda permitirá um artefato. Mas você pode recorrer ao método Chetan Jags , que permite reduzir a manifestação de um artefato. Para fazer isso, no estágio da convolução preliminar, a seleção no mapa do ambiente não é realizada diretamente, mas em um de seus níveis mip, com base no valor obtido da função de distribuição de probabilidade do integrando e da rugosidade:

 float D = DistributionGGX(NdotH, roughness); float pdf = (D * NdotH / (4.0 * HdotV)) + 0.0001; //        float resolution = 512.0; float saTexel = 4.0 * PI / (6.0 * resolution * resolution); float saSample = 1.0 / (float(SAMPLE_COUNT) * pdf + 0.0001); float mipLevel = roughness == 0.0 ? 0.0 : 0.5 * log2(saSample / saTexel); 

Lembre-se de ativar a filtragem trilinear para o mapa do ambiente para selecionar com êxito nos níveis mip:

 glBindTexture(GL_TEXTURE_CUBE_MAP, envCubemap); glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR); 

Além disso, não esqueça de criar diretamente níveis de mip para a textura usando o OpenGL, mas somente depois que o nível principal de mip estiver totalmente formado:

 //  HDR      ... [...] //  - glBindTexture(GL_TEXTURE_CUBE_MAP, envCubemap); glGenerateMipmap(GL_TEXTURE_CUBE_MAP); 

Esse método funciona surpreendentemente bem, removendo quase todos (e geralmente todos) os pontos do mapa filtrado, mesmo em altos níveis de rugosidade.

Cálculo preliminar de BRDF


Portanto, processamos com sucesso o mapa do ambiente com o filtro e agora podemos nos concentrar na segunda parte da aproximação na forma de uma soma separada representando o BRDF. Para atualizar a memória, verifique novamente o registro completo da solução aproximada:

L O ( p , ω o ) = ohms L i ( p , ω i ) d w i * ohms f r ( p , ω i , ω O ) n ω i d co i


Calculamos preliminarmente a parte esquerda da soma e registramos os resultados para vários níveis de rugosidade em um mapa cúbico separado. O lado direito exigirá a convolução da expressão BDRF junto com os seguintes parâmetros: anglen ω i , rugosidade da superfície e coeficiente de FresnelF 0 . Um processo semelhante à integração de um BRDF espelhado para um ambiente completamente branco ou com brilho de energia constante L i = 1,0 . A participação do BRDF para três variáveis ​​não é uma tarefa trivial, mas neste caso F 0 pode ser derivado da expressão que descreve o espelho BRDF:

ohms fr(p,ωi,ωO)nωidωi=ohms fr(p,ωi,ωO) F ( ω o , h )F ( ω o , h ) nωidcoi


Aqui F é uma função que descreve o cálculo do conjunto de Fresnel. Movendo o divisor para a expressão do BRDF, você pode ir para a seguinte notação equivalente:

ohms f r ( p , ω i , ω o )F ( ω o , h ) F(ωo,h)nωidcoi


Substituindo a entrada direita F na aproximação de Fresnel-Schlick, obtemos:

ohms f r ( p , ω i , ω o )F ( ω o , h ) (F0+(1-F0)(1-ωoh)5)nωidωi


Denotar a expressão ( 1 - ω oh ) 5 como / a l p h a para simplificar a decisão sobreF 0 :

Ωfr(p,ωi,ωo)F(ωo,h)(F0+(1F0)α)nωidωi


Ωfr(p,ωi,ωo)F(ωo,h)(F0+1αF0α)nωidωi


Ωfr(p,ωi,ωo)F ( ω o , h ) (F0*(1-α)+α)nωidcoi


Próxima função Dividimos F em duas integrais:

ohms f r ( p , ω i , ω o )F ( ω o , h ) (F0*(1-α))nωidcoi+ohmsfr(p,ωi,ωo)F ( ω o , h ) (α)nωidcoi


Desta maneira F 0 será constante sob a integral, e podemos retirá-la do sinal da integral. A seguir, revelaremosα na expressão original e obtenha a entrada final para BRDF como uma soma separada:

F0Ωfr(p,ωi,ωo)(1(1ωoh)5)nωidωi+Ωfr(p,ωi,ωo)(1ωoh)5nωidωi


As duas integrais resultantes representam a escala e o deslocamento para o valor F 0, respectivamente. Note quef ( p , ω i , ω o ) contém uma ocorrênciaF , porque essas ocorrências se cancelam e desaparecem da expressão. Usando a abordagem já desenvolvida, a convolução BRDF pode ser realizada juntamente com os dados de entrada: rugosidade e ângulo entre vetores

n e de w o .Escrever o resultado na textura 2D - cartão BRDF complexação ( BRDF mapa integração ), que servirá de valores da tabela auxiliares para uso no shader final, que formarão o resultado final da iluminação especular indireta.

O sombreador de convolução BRDF trabalha no plano, usando diretamente coordenadas bidimensionais de textura como parâmetros de entrada do processo de convolução ( NdotV e rugosidade ). O código é notavelmente semelhante a uma convolução da pré-filtragem, mas aqui o vetor de amostra é processado levando em consideração a função geométrica BRDF e a expressão de aproximação de Fresnel-Schlick:

 vec2 IntegrateBRDF(float NdotV, float roughness) { vec3 V; Vx = sqrt(1.0 - NdotV*NdotV); Vy = 0.0; Vz = NdotV; float A = 0.0; float B = 0.0; vec3 N = vec3(0.0, 0.0, 1.0); const uint SAMPLE_COUNT = 1024u; for(uint i = 0u; i < SAMPLE_COUNT; ++i) { vec2 Xi = Hammersley(i, SAMPLE_COUNT); vec3 H = ImportanceSampleGGX(Xi, N, roughness); vec3 L = normalize(2.0 * dot(V, H) * H - V); float NdotL = max(Lz, 0.0); float NdotH = max(Hz, 0.0); float VdotH = max(dot(V, H), 0.0); if(NdotL > 0.0) { float G = GeometrySmith(N, V, L, roughness); float G_Vis = (G * VdotH) / (NdotH * NdotV); float Fc = pow(1.0 - VdotH, 5.0); A += (1.0 - Fc) * G_Vis; B += Fc * G_Vis; } } A /= float(SAMPLE_COUNT); B /= float(SAMPLE_COUNT); return vec2(A, B); } // ---------------------------------------------------------------------------- void main() { vec2 integratedBRDF = IntegrateBRDF(TexCoords.x, TexCoords.y); FragColor = integratedBRDF; } 

Como você pode ver, a convolução do BRDF é implementada como um arranjo quase literal dos cálculos matemáticos acima. Os parâmetros de entrada de rugosidade e ângulo são obtidos.θ , um vetor de amostra é formado com base na amostra por significância, processado usando a função de geometria e a expressão de Fresnel convertida para BRDF. Como resultado, para cada amostra, a magnitude da escala e deslocamento do valorF 0 , que no final são calculados como média e retornados no formatovec2. Em umaliçãoteórica, foi mencionado que o componente geométrico do BRDF é ligeiramente diferente no caso do cálculo da IBL, uma vez que o coeficiente

k é especificado de forma diferente:

k d i r e c t = ( α + 1 ) 28


k I B L = α 22


Como a convolução BRDF faz parte da solução da integral no caso do cálculo do IBL, usaremos o coeficiente k I B L para calcular a função de geometria no modelo Schlick-GGX:

 float GeometrySchlickGGX(float NdotV, float roughness) { float a = roughness; float k = (a * a) / 2.0; float nom = NdotV; float denom = NdotV * (1.0 - k) + k; return nom / denom; } // ---------------------------------------------------------------------------- float GeometrySmith(vec3 N, vec3 V, vec3 L, float roughness) { float NdotV = max(dot(N, V), 0.0); float NdotL = max(dot(N, L), 0.0); float ggx2 = GeometrySchlickGGX(NdotV, roughness); float ggx1 = GeometrySchlickGGX(NdotL, roughness); return ggx1 * ggx2; } 

Observe que o coeficiente k é calculado com base no parâmetroa. Além disso, nesse caso, o parâmetro derugosidadenãoéelevado ao quadrado ao descrever o parâmetroa, o que foi feito em outros locais onde esse parâmetro foi aplicado. Não sei ao certo onde está o problema: no trabalho da Epic Games ou no trabalho inicial da Disney, mas vale a pena dizer que é precisamente essa atribuição direta do valor darugosidadeaoparâmetroa quecria o mapa de integração BRDF idêntico, apresentado na publicação da Epic Games.Além disso, os resultados da convolução do BRDF serão salvos na forma de uma textura 2D do tamanho 512x512:



 unsigned int brdfLUTTexture; glGenTextures(1, &brdfLUTTexture); //   ,     glBindTexture(GL_TEXTURE_2D, brdfLUTTexture); glTexImage2D(GL_TEXTURE_2D, 0, GL_RG16F, 512, 512, 0, GL_RG, GL_FLOAT, 0); 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); 

Conforme recomendado pela Epic Games, um formato de textura de ponto flutuante de 16 bits é usado aqui. Certifique-se de definir o modo de repetição como GL_CLAMP_TO_EDGE para evitar a amostragem de artefatos da borda.

Em seguida, usamos o mesmo objeto de buffer de quadro e executamos um sombreador na superfície de um quad de tela cheia:

 glBindFramebuffer(GL_FRAMEBUFFER, captureFBO); glBindRenderbuffer(GL_RENDERBUFFER, captureRBO); glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH_COMPONENT24, 512, 512); glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, brdfLUTTexture, 0); glViewport(0, 0, 512, 512); brdfShader.use(); glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); RenderQuad(); glBindFramebuffer(GL_FRAMEBUFFER, 0); 

Como resultado, obtemos um mapa de textura que armazena o resultado da convolução da parte da expressão do valor da divisão responsável pelo BRDF:


Tendo em mãos os resultados da filtragem preliminar do mapa do ambiente e a textura com os resultados da convolução BRDF, podemos restaurar o resultado do cálculo da integral para iluminação especular indireta com base na aproximação por uma soma separada. O valor restaurado será subsequentemente usado como radiação especular indireta ou de fundo.

Cálculo final da refletância no modelo IBL


Portanto, para obter um valor que descreva o componente espelho indireto na expressão geral de refletividade, é necessário "colar" os componentes de aproximação calculados em um único todo como uma soma separada. Primeiro, adicione os amostradores apropriados ao shader final para dados pré-calculados:

 uniform samplerCube prefilterMap; uniform sampler2D brdfLUT; 

Primeiro, obtemos o valor da reflexão especular indireta na superfície por amostragem de um mapa de ambiente pré-processado com base no vetor de reflexão. Observe que aqui a seleção do nível mip para amostragem é baseada na rugosidade da superfície. Para superfícies mais ásperas, o reflexo será mais desfocado :

 void main() { [...] vec3 R = reflect(-V, N); const float MAX_REFLECTION_LOD = 4.0; vec3 prefilteredColor = textureLod(prefilterMap, R, roughness * MAX_REFLECTION_LOD).rgb; [...] } 

No estágio de convolução preliminar, preparamos apenas 5 níveis de mip (de zero a quarto), a constante MAX_REFLECTION_LOD serve para limitar a seleção dos níveis de mip gerados.

Em seguida, fazemos uma seleção no mapa de integração do BRDF com base na rugosidade e ângulo entre o normal e a direção da vista:

 vec3 F = FresnelSchlickRoughness(max(dot(N, V), 0.0), F0, roughness); vec2 envBRDF = texture(brdfLUT, vec2(max(dot(N, V), 0.0), roughness)).rg; vec3 specular = prefilteredColor * (F * envBRDF.x + envBRDF.y); 

O valor obtido no mapa contém os fatores de escala e deslocamento para o valor F 0 (aqui assumimos o valorF- coeficiente de Fresnel). O valor convertidoF éentão combinado com o valor obtido no mapa de pré-filtragem para obter uma solução aproximada da expressão integral original -especular.Assim, obtemos uma solução para a parte da expressão da refletividade, responsável pela reflexão especular. Para obter uma solução completa do modelo PBR IBL, você precisa combinar esse valor com a solução da parte difusa da expressão de refletância que recebemos naúltimalição:



 vec3 F = FresnelSchlickRoughness(max(dot(N, V), 0.0), F0, roughness); vec3 kS = F; vec3 kD = 1.0 - kS; kD *= 1.0 - metallic; vec3 irradiance = texture(irradianceMap, N).rgb; vec3 diffuse = irradiance * albedo; const float MAX_REFLECTION_LOD = 4.0; vec3 prefilteredColor = textureLod(prefilterMap, R, roughness * MAX_REFLECTION_LOD).rgb; vec2 envBRDF = texture(brdfLUT, vec2(max(dot(N, V), 0.0), roughness)).rg; vec3 specular = prefilteredColor * (F * envBRDF.x + envBRDF.y); vec3 ambient = (kD * diffuse + specular) * ao; 

Noto que o valor especular não é multiplicado por kS , pois já contém um coeficiente de Fresnel.

Vamos executar nosso aplicativo de teste com um conjunto familiar de esferas com características variáveis ​​de metalicidade e rugosidade e dar uma olhada em sua aparência no esplendor completo da PBR:


Você pode ir ainda mais longe e baixar um conjunto de texturas correspondentes ao modelo PBR e obter esferas de materiais reais :


Ou mesmo faça o download de um modelo deslumbrante, juntamente com as texturas PBR preparadas de Andrew Maximov :


Acho que você não precisa convencer ninguém de que o atual modelo de iluminação parece muito mais convincente. Além disso, a iluminação parece fisicamente correta, independentemente do mapa do ambiente. Abaixo, são usados ​​vários mapas de ambiente HDR completamente diferentes que alteram completamente a natureza da iluminação - mas todas as imagens parecem fisicamente confiáveis, apesar do fato de você não ter que ajustar nenhum parâmetro no modelo! (Em princípio, essa simplificação do trabalho com materiais é a principal vantagem do pipeline de PBR, e uma imagem melhor pode ser considerada uma consequência agradável. Nota. )


Nossa viagem para a essência do renderizador PBR é bastante volumosa. Chegamos ao resultado através de uma série de etapas e, é claro, muita coisa pode dar errado nas primeiras abordagens. Portanto, para qualquer problema, recomendo que você compreenda cuidadosamente o código de exemplo para esferas monocromáticas e texturizadas (e o código de sombreamento, é claro!). Ou peça conselhos nos comentários.

O que vem a seguir?


Espero que, lendo estas linhas, você já tenha desenvolvido um entendimento do trabalho do modelo de renderização da PBR, bem como tenha descoberto e lançado com êxito um aplicativo de teste. Nestas lições, calculamos todos os mapas auxiliares de textura necessários para o modelo PBR em nosso aplicativo antes do ciclo de renderização principal. Para tarefas de treinamento, essa abordagem é adequada, mas não para aplicação prática. Em primeiro lugar, essa preparação preliminar deve ocorrer uma vez, e nem todo lançamento de aplicativo. Em segundo lugar, se você decidir adicionar mais alguns mapas de ambiente, também será necessário processá-los na inicialização. E se mais alguns cartões forem adicionados? Bola de neve real.

É por isso que, no caso geral, um mapa de irradiação e um mapa de ambiente pré-processado são preparados uma vez e salvos no disco (o mapa de agregação BRDF não depende do mapa do ambiente, para que geralmente possa ser calculado ou baixado uma vez). Daqui resulta que você precisará de um formato para armazenar cartões cúbicos HDR, incluindo seus níveis mip. Bem, ou você pode armazená-los e carregá-los usando um dos formatos mais usados ​​(o .dds suporta a economia de níveis de mip).

Outro ponto importante: para fornecer um entendimento profundo do pipeline de PBR nessas lições, descrevi o processo completo de preparação para a renderização de PBR, incluindo cálculos preliminares de cartões auxiliares para IBL. No entanto, em sua prática, você também pode usar um dos grandes utilitários que preparam esses cartões para você: por exemplo, cmftStudio ou IBLBaker .

Nós também não considerou o processo de preparação mapa cubo amostras reflexões ( sondas de reflexão) e os processos relacionados de interpolação de mapas cúbicos e correção de paralaxe. Resumidamente, essa técnica pode ser descrita da seguinte maneira: colocamos em nossa cena muitos objetos de amostras de reflexão, que formam uma imagem ambiental local na forma de um mapa cúbico e, em seguida, todos os mapas auxiliares necessários para o modelo de IBL são formados com base. Ao interpolar dados de várias amostras com base na distância da câmera, você pode obter uma iluminação altamente detalhada com base na imagem, cuja qualidade é essencialmente limitada apenas pelo número de amostras que estamos prontos para colocar na cena. Essa abordagem permite alterar corretamente a iluminação, por exemplo, ao passar de uma rua bem iluminada para o crepúsculo de uma determinada sala. Provavelmente vou escrever uma lição sobre testes de reflexão no futuro,No entanto, no momento, só posso recomendar o artigo de Chetan Jags abaixo para revisão.

(A implementação de amostras e muito mais pode ser encontrada no mecanismo bruto do autor dos tutoriais aqui , aprox. Por. )

Materiais adicionais


  1. Sombreamento real no Unreal Engine 4 : Uma explicação da abordagem da Epic Games para aproximar a expressão do componente espelho por uma soma dividida. Com base neste artigo, o código da lição IBL PBR foi escrito.
  2. Sombreamento com base física e iluminação com base em imagem : Um excelente artigo que descreve o processo de inclusão do cálculo do componente espelho do IBL em um aplicativo de pipeline interativo PBR.
  3. Iluminação baseada em imagem : um post muito longo e detalhado sobre IBL especular e questões relacionadas, incluindo o problema de interpolação da sonda de luz.
  4. Moving Frostbite to PBR : , PBR «AAA».
  5. Physically Based Rendering – Part Three : , IBL PBR JMonkeyEngine.
  6. Implementation Notes: Runtime Environment Map Filtering for Image Based Lighting : HDR , .

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


All Articles