
A lição
anterior deu uma visão geral dos conceitos básicos da implementação de um modelo de renderização fisicamente plausível. Desta vez, passaremos dos cálculos teóricos para uma implementação de renderização específica com a participação de fontes de luz diretas (analíticas): ponto, direcional ou holofote.
Primeiro, vamos atualizar a expressão para calcular a refletividade da lição anterior:
Lo(p, omegao)= int limits Omega(kd fracc pi+ fracDFG4( omegao cdotn)( omegai cdotn))Li(p, omegai)n cdot omegaid omegai
Na maioria das vezes, já lidamos com os componentes dessa fórmula, mas permanece a questão de como representar especificamente a
irradiância , que é o brilho total da energia (
brilho )
L toda a cena. Concordamos que o brilho da energia
L (em termos de terminologia da computação gráfica) é considerada a razão do fluxo de radiação (
fluxo radiante )
phi (energia de radiação da fonte de luz) para o valor do ângulo sólido
omega . No nosso caso, o ângulo sólido
omega consideramos infinitesimal e, portanto, o brilho da energia dá uma idéia do fluxo de radiação para cada raio de luz individual (sua direção).
Como vincular esses cálculos ao modelo de iluminação que conhecemos das lições anteriores? Primeiro, imagine que você receba uma fonte pontual de luz (que emite uniformemente em todas as direções) com um fluxo de radiação definido como uma tríade RGB (23.47, 21.31, 20.79). A
intensidade radiante dessa fonte é igual ao seu fluxo de radiação em todas as direções. No entanto, tendo considerado o problema de determinar a cor de um ponto específico
p na superfície, você pode ver que todas as direções possíveis de incidência de luz no hemisfério
Omega único vetor
wi obviamente virá de uma fonte de luz. Como apenas uma fonte de luz é representada, representada por um ponto no espaço, para todas as outras direções possíveis de incidência de luz em um ponto
p o brilho da energia será igual a zero:
Agora, se não considerarmos temporariamente a lei da atenuação da luz para uma dada fonte, verifica-se que o brilho da energia para o feixe de luz incidente dessa fonte permanece inalterado onde quer que a fonte seja colocada (escala de luminosidade baseada no cosseno do ângulo de incidência)
phi também não conta). No total, uma fonte pontual mantém a força de radiação constante, independentemente do ângulo de visão, o que equivale a tomar a força de radiação igual ao fluxo de radiação inicial na forma de uma constante de tríade (23.47, 21.31, 20.79).
No entanto, o cálculo do brilho da energia também é baseado na coordenada do ponto
p , pelo menos qualquer fonte de luz fisicamente confiável mostra a atenuação da força de radiação com o aumento da distância de um ponto a uma fonte. Você também deve levar em consideração a orientação da superfície, como pode ser visto na expressão original de luminosidade: o resultado do cálculo da força de radiação deve ser multiplicado pelo valor escalar do vetor normal para a superfície
n e vetor de incidência de radiação
wi .
Para reescrever o acima: para uma fonte direta de luz, a função de radiação
L determina a cor da luz incidente, levando em consideração a atenuação a uma determinada distância do ponto
p e levando em consideração a escala por um fator
n cdotwi mas apenas para um único raio de luz
wi chegando ao ponto
p - essencialmente o único vetor que conecta a fonte e o ponto. Na forma de código-fonte, isso é interpretado da seguinte maneira:
vec3 lightColor = vec3(23.47, 21.31, 20.79); vec3 wi = normalize(lightPos - fragPos); float cosTheta = max(dot(N, Wi), 0.0); float attenuation = calculateAttenuation(fragPos, lightPos); vec3 radiance = lightColor * attenuation * cosTheta;
Se você fechar os olhos para uma terminologia ligeiramente modificada, esse trecho de código deve lembrá-lo de algo. Sim, sim, esse é o mesmo código para calcular o componente difuso no modelo de iluminação conhecido por nós. Para iluminação direta, o brilho da energia é determinado por um único vetor para a fonte de luz, porque o cálculo é realizado de maneira tão semelhante à que ainda sabemos.
Observo que esta afirmação é verdadeira apenas sob a suposição de que uma fonte pontual de luz é infinitesimal e é representada por um ponto no espaço. Ao modelar uma fonte de volume, sua luminosidade será diferente de zero em várias direções, e não apenas em um feixe.
Para outras fontes de luz que emitem radiação a partir de um único ponto, o brilho da energia é calculado da mesma maneira. Por exemplo, uma fonte de luz direcional tem uma direção constante
wi e não usa atenuação, e a fonte de projeção mostra uma potência de radiação variável, dependendo da direção da fonte.
Aqui voltamos ao valor da integral
int na superfície do hemisfério
Omega . Como conhecemos antecipadamente as posições de todas as fontes de luz que participam do sombreamento de um ponto específico, não precisamos tentar resolver a integral. Podemos calcular diretamente a irradiação total fornecida por esse número de fontes de luz, pois o brilho da energia da superfície é afetado por uma única direção para cada fonte.
Como resultado, o cálculo de PBR para fontes de luz direta é uma questão bastante simples, pois tudo se resume a uma pesquisa seqüencial das fontes envolvidas na iluminação. Posteriormente, um componente do ambiente aparecerá no modelo de iluminação, sobre o qual trabalharemos no tutorial sobre iluminação baseada em imagem (
Iluminação Baseada em Imagem ,
IBL ). Não há como escapar da estimativa da integral, uma vez que a luz nesse modelo cai de várias direções.
Modelo de superfície PBR
Vamos começar com o shader de fragmento que implementa o modelo PBR descrito acima. Primeiro, configuramos os dados de entrada necessários para o sombreamento da superfície:
#version 330 core out vec4 FragColor; in vec2 TexCoords; in vec3 WorldPos; in vec3 Normal; uniform vec3 camPos; uniform vec3 albedo; uniform float metallic; uniform float roughness; uniform float ao;
Aqui você pode ver a entrada usual calculada usando o sombreador de vértice mais simples, bem como um conjunto de uniformes que descrevem as características da superfície do objeto.
Além disso, no início do código do sombreador, realizamos cálculos tão familiares com a implementação do modelo de iluminação Blinn-Fong:
void main() { vec3 N = normalize(Normal); vec3 V = normalize(camPos - WorldPos); [...] }
Iluminação direta
O exemplo desta lição contém apenas quatro fontes de luz pontuais que especificam claramente a irradiação da cena. Para satisfazer a expressão da refletividade, percorremos iterativamente cada fonte de luz, calculamos o brilho da energia individual e resumimos essa contribuição, modulando simultaneamente o valor do BRDF e o ângulo de incidência do feixe de luz. Você pode imaginar essa iteração como uma solução da integral sobre a superfície
Omega Apenas para fontes de luz analíticas.
Portanto, primeiro calculamos os valores calculados para cada fonte:
vec3 Lo = vec3(0.0); for(int i = 0; i < 4; ++i) { vec3 L = normalize(lightPositions[i] - WorldPos); vec3 H = normalize(V + L); float distance = length(lightPositions[i] - WorldPos); float attenuation = 1.0 / (distance * distance); vec3 radiance = lightColors[i] * attenuation; [...]
Como os cálculos são realizados no espaço linear (
a correção gama é realizada no final do sombreador), uma lei de atenuação fisicamente mais correta é usada de acordo com o quadrado inverso da distância:
Suponha que a lei do quadrado inverso seja mais fisicamente correta, para controlar melhor a natureza do amortecimento, é bem possível usar a já familiar fórmula que contém termos constantes, lineares e quadráticos.
Além disso, para cada fonte, também calculamos o valor do espelho Cook-Torrance BRDF:
fracDFG4( omegao cdotn)( omegai cdotn)
O primeiro passo é calcular a proporção entre reflexão especular e reflexão difusa ou, em outras palavras, a proporção entre a quantidade de luz refletida e a quantidade de luz refratada pela superfície. Na
lição anterior, sabemos como é o cálculo do coeficiente de Fresnel:
vec3 fresnelSchlick(float cosTheta, vec3 F0) { return F0 + (1.0 - F0) * pow(1.0 - cosTheta, 5.0); }
A aproximação de Fresnel-Schlick espera o parâmetro
F0 na entrada, que mostra o
grau de reflexão da superfície no ângulo zero de incidência de luz , ou seja, grau de reflexão, se você olhar para a superfície ao longo do normal de cima para baixo. O valor de
F0 varia de acordo com o material e adquire um tom de cor para os metais, como pode ser observado nos catálogos de materiais PBR. Para o processo de
fluxo de trabalho metálico (o processo de autoria dos materiais PBR, dividindo todos os materiais em classes de dielétricos e condutores), supõe-se que todos os dielétricos pareçam razoavelmente confiáveis com um valor constante de
F0 = 0,04 , enquanto que para superfícies metálicas
F0 é definido com base no albedo da superfície. Sob a forma de código:
vec3 F0 = vec3(0.04); F0 = mix(F0, albedo, metallic); vec3 F = fresnelSchlick(max(dot(H, V), 0.0), F0);
Como você pode ver, para superfícies estritamente não metálicas,
F0 é igual a 0,04. Mas, ao mesmo tempo, pode mudar suavemente desse valor para o valor de albedo com base na “metalicidade” da superfície. Esse indicador geralmente é apresentado como uma textura separada (a partir daqui, de fato, o fluxo de trabalho
metálico é obtido,
aprox. Trans. ).
Tendo recebido
F precisamos calcular o valor da função de distribuição normal
D funções de geometria
G :
Código de função do gabinete com iluminação analítica:
float DistributionGGX(vec3 N, vec3 H, float roughness) { float a = roughness*roughness; float a2 = a*a; float NdotH = max(dot(N, H), 0.0); float NdotH2 = NdotH*NdotH; float num = a2; float denom = (NdotH2 * (a2 - 1.0) + 1.0); denom = PI * denom * denom; return num / denom; } float GeometrySchlickGGX(float NdotV, float roughness) { float r = (roughness + 1.0); float k = (r*r) / 8.0; float num = NdotV; float denom = NdotV * (1.0 - k) + k; return num / 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; }
Uma diferença importante da descrita na
parte teórica : aqui passamos diretamente o parâmetro de rugosidade para todas as funções mencionadas. Isso é feito para permitir que cada função modifique o valor da rugosidade original à sua maneira. Por exemplo, estudos da Disney, refletidos no mecanismo da Epic Games, mostraram que o modelo de iluminação fornece resultados visualmente mais corretos se usarmos o quadrado da rugosidade na função de geometria e na função de distribuição normal.
Após definir todas as funções, é possível obter diretamente os valores de NDF e G:
float NDF = DistributionGGX(N, H, roughness); float G = GeometrySmith(N, V, L, roughness);
Total, temos em mãos todos os valores para o cálculo de todo o BRDF da Cook-Torrance:
vec3 numerator = NDF * G * F; float denominator = 4.0 * max(dot(N, V), 0.0) * max(dot(N, L), 0.0) + 0.001; vec3 specular = numerator / denominator;
Observe que limitamos o denominador a um valor mínimo de 0,001 para impedir a divisão por zero nos casos de zerar o produto escalar.
Agora, procedemos ao cálculo da contribuição de cada fonte para a equação da refletividade. Como o coeficiente de Fresnel é diretamente uma variável
Ks , então podemos usar o valor de F para indicar a contribuição da fonte para a reflexão especular da superfície. A partir da quantidade
Ks pode ser obtido e o índice de refração
Kd :
vec3 kS = F; vec3 kD = vec3(1.0) - kS; kD *= 1.0 - metallic;
Como consideramos a quantidade
kS representando a quantidade de energia luminosa como uma superfície refletida, subtraindo-a da unidade, obtemos a energia residual da luz
kD refratada pela superfície. Além disso, como os metais não refratam a luz e não possuem um componente difuso da luz reemitida, o componente
kD será modulado para zero para um material totalmente metálico. Após esses cálculos, teremos todos os dados disponíveis para calcular a refletância fornecida por cada uma das fontes de luz:
const float PI = 3.14159265359; float NdotL = max(dot(N, L), 0.0); Lo += (kD * albedo / PI + specular) * radiance * NdotL; }
O valor final
Lo , ou brilho de energia de saída, é essencialmente uma solução para a expressão de refletividade, ou seja, resultado de integração da superfície
Omega . Nesse caso, não precisamos tentar resolver a integral de uma forma geral para todas as direções possíveis, pois neste exemplo existem apenas quatro fontes de luz que afetam o fragmento que está sendo processado. É por isso que toda "integração" é limitada a um ciclo simples de fontes de luz existentes.
Resta apenas adicionar a semelhança do componente de iluminação de fundo aos resultados do cálculo da fonte de luz direta e a cor final do fragmento está pronta:
vec3 ambient = vec3(0.03) * albedo * ao; vec3 color = ambient + Lo;
Renderização linear e HDR
Até agora, assumimos que todos os cálculos são realizados em um espaço de cores linear e, portanto, usamos
a correção gama como o acorde final em nosso sombreador. A realização de cálculos no espaço linear é extremamente importante para a simulação correta do PBR, pois o modelo requer a linearidade de todos os dados de entrada. Tente não garantir a linearidade de nenhum dos parâmetros e o resultado do sombreamento estará incorreto. Além disso, seria bom definir as fontes de luz com características próximas das fontes reais: por exemplo, a cor de sua radiação e o brilho da energia podem variar livremente em uma ampla faixa. Como resultado,
Lo pode facilmente aceitar valores grandes, mas inevitavelmente cai abaixo do ponto de corte no intervalo [0., 1.] devido à baixa faixa dinâmica (
LDR ) do buffer de quadro padrão.
Para evitar a perda dos valores HDR, antes da correção gama, é necessário realizar a compactação de tom:
color = color / (color + vec3(1.0)); color = pow(color, vec3(1.0/2.2));
O familiar operador Reinhardt é usado aqui, o que nos permite manter uma ampla faixa dinâmica sob condições de grande alteração na irradiação de diferentes partes da imagem. Como aqui não usamos um shader separado para o pós-processamento, as operações descritas podem ser adicionadas simplesmente ao final do código do shader.
Repito que, para a modelagem correta do PBR, é extremamente importante lembrar e considerar os recursos de trabalhar com espaço de cores linear e renderização HDR. Negligenciar esses aspectos levará a cálculos incorretos e a resultados visualmente não estéticos.
Shader PBR para iluminação analítica
Portanto, juntamente com os toques finais na forma de compactação tonal e correção de gama, resta apenas transferir a cor final do fragmento para a saída do shader de fragmento e o código de shader PBR para iluminação direta pode ser considerado concluído. Finalmente, vamos dar uma olhada em todo o código da função
main () deste shader:
#version 330 core out vec4 FragColor; in vec2 TexCoords; in vec3 WorldPos; in vec3 Normal;
Espero que, depois de ler a
parte teórica e com a análise de hoje da expressão da capacidade reflexiva, essa listagem deixe de parecer intimidadora.
Usamos esse sombreador em uma cena que contém fontes de luz de quatro pontos, um certo número de esferas cujas características da superfície alteram o grau de sua rugosidade e metalicidade ao longo dos eixos horizontal e vertical, respectivamente. Na saída, temos a seguinte imagem:
A metalicidade muda de zero para um, de baixo para cima, e a rugosidade é semelhante, mas da esquerda para a direita. Torna-se evidente que, alterando apenas essas duas características da superfície, já é possível definir uma ampla gama de materiais.
O código fonte completo está
aqui .
PBR e texturização
Expandiremos nosso modelo de superfície transmitindo características na forma de texturas. Dessa forma, podemos fornecer controle por fragmento dos parâmetros do material da superfície:
[...] uniform sampler2D albedoMap; uniform sampler2D normalMap; uniform sampler2D metallicMap; uniform sampler2D roughnessMap; uniform sampler2D aoMap; void main() { vec3 albedo = pow(texture(albedoMap, TexCoords).rgb, 2.2); vec3 normal = getNormalFromNormalMap(); float metallic = texture(metallicMap, TexCoords).r; float roughness = texture(roughnessMap, TexCoords).r; float ao = texture(aoMap, TexCoords).r; [...] }
Observe que a textura albedo da superfície geralmente é criada por artistas no espaço de cores sRGB; portanto, no código acima, retornamos a cor texel ao espaço linear para que possa ser usada em cálculos adicionais. Dependendo de como os artistas criam a textura que contém os dados do
mapa de oclusão ambiental , também pode ser necessário trazê-lo para o espaço linear. Mapas de metalicidade e rugosidade quase sempre são criados no espaço linear.
O uso de texturas em vez de parâmetros fixos da superfície em combinação com o algoritmo PBR proporciona um aumento significativo na confiabilidade visual em comparação com os algoritmos de iluminação usados anteriormente:
O código de exemplo de textura completa está
aqui e as texturas usadas estão
aqui (junto com a textura de sombreamento de fundo). Chamo a atenção para o fato de que superfícies fortemente metálicas parecem escurecidas sob condições de iluminação direta, uma vez que a contribuição da reflexão difusa é pequena (no limite, não há nenhuma). Seu sombreamento se torna mais correto somente quando se considera o reflexo do espelho da iluminação do ambiente, o que faremos nas próximas lições.
No momento, o resultado pode não ser tão impressionante quanto algumas demonstrações de PBR - ainda não implementamos um sistema de iluminação baseado em imagem (
IBL ). No entanto, nossa renderização agora é considerada baseada em princípios físicos e, mesmo sem o IBL, mostra uma imagem mais confiável do que antes.