1 Introdução
Isso é o seguimento de Blinn Phong com Texturas de Roughness
1.1 Cores lavadas
Em texturas bem escuras e bem ásperas (roughness alto) ainda pode ter um pequeno specular fazendo a textura parecer lavada, isso pode ser indesejável se a textura não foi feita pra ter specular, como sprites do doom ou texturas do half life 1.
O que nós podemos fazer é calcular a normalização para o menor shininess que podemos ter, que é 1.0, e então usar ele para mover a normalização.
float normalization = ((shininess + 2.0) * (shininess + 4.0)) / (8.0 * PI * (pow(2.0, -shininess * 0.5) + shininess));
normalization = max(normalization - 0.3496155267919281, 0.0);
A função max é para evitar que a normalização se torne negativa e cause comportamento indefinido (por causa da imprecisão de float).
A textura agora está completamente preta, como deveria ser.
1.2 Conservação de Energia Difusa
De Energy Conservation In Games (em inglês)
O autor sugere dividir o diffuse do material por PI mas isso iria tornar a cena inteira mais escura que então iria necessitar da luz ser multiplicada por PI para que possa retornar ao seu estado de brilho anterior.
Ao invés de fazer isso, nós podemos simplesmente multiplicar a normalização por PI, que seria o mesmo que dividir o diffuse por PI e então aumentar a intensidade da luz por PI.
float normalization = ((shininess + 2.0) * (shininess + 4.0)) / (8.0 * PI * (pow(2.0, -shininess * 0.5) + shininess));
normalization = max(normalization - 0.3496155267919281, 0.0) * PI;
É assim que se parece agora, com um especular mais forte.
2 Textura Metallic
Então, uma textura de metallic (ou metalness) define se a superfície é um metal ou não mas como ela normalmente vai de 0 a 255 ou de 0 a 1 (se normalizada) e como não existe tal coisa como "meio metal", em prática ela define uma interpolação linear entre a versão não metálica do material e a versão metálica do material.
vec3 outputColor = mix(nonMetallicOutputColor, metallicOutputColor, metallic);
3 Metais
Então, o que faz um metal se parecer com um metal? vamos olhar a coisa mais metálica e brilhante que todo mundo tem, um espelho.
Olhando um espelho nós vemos duas coisas:
É totalmente refletivo.
Não tem diffuse ou sombras (olhe as sombras na superfície de "madeira").
Agora, vamos olhar outro exemplo (isso é de um doce, mas tem as propriedades de um metal):
E é isso que nós vemos:
Ele tem um pouco de diffuse, então não é totalmente metálico.
A cor do reflexo parece ter a mesma cor que a sua superfície.
Então, dessas pequenas observações nós vemos que:
Metais são totalmente refletivos.
Metais quase não possuem diffuse.
A cor do reflexo tem a mesma cor do metal.
4 Em código
É assim que essa textura de cobre atualmente se parece.
Não se parece com um metal, se parece como plástico.
4.1 Iluminação
Então, o que nós temos que fazer é na verdade bem simples, nós só precisamos multiplicar o specularFactor pelo diffuseColor e misturar (função mix) com a soma atual de componentes de luz usando o valor de metallic, também temos que nos lembrar de mover ambos DIFFUSE_INTENSITY e SPECULAR_INTENSITY para o cálculo de luz do diffuse e do specular já que um metal é 0.0 diffuse e 1.0 specular.
Então isso
diffuseFactor *= DIFFUSE_INTENSITY;
specularFactor *= SPECULAR_INTENSITY;
vec3 diffuse = diffuseFactor * diffuseColor;
vec3 specular = specularFactor * specularColor;
vec3 ambient = 0.10 * diffuseColor;
return diffuse + specular + ambient;
Se torna isso
vec3 diffuse = diffuseFactor * diffuseColor * DIFFUSE_INTENSITY;
vec3 specular = specularFactor * specularColor * SPECULAR_INTENSITY;
vec3 ambient = 0.10 * diffuseColor;
return mix(diffuse + specular + ambient, specularFactor * diffuseColor, metallic);
E o código completo é:
vec3 directionalLight(
vec3 lightDirection, vec3 viewDirection, vec3 normal,
vec3 diffuseColor, vec3 specularColor,
float roughness, float metallic
) {
float shininess = pow(MAX_SHININESS, 1.0 - roughness);
float normalization = ((shininess + 2.0) * (shininess + 4.0)) / (8.0 * PI * (pow(2.0, -shininess * 0.5) + shininess));
normalization = max(normalization - 0.3496155267919281, 0.0) * PI;
vec3 halfwayDirection = -normalize(lightDirection + viewDirection);
float diffuseFactor = max(dot(normal, -lightDirection), 0.0);
float specularFactor = pow(max(dot(normal, halfwayDirection), 0.0), shininess) * diffuseFactor * normalization;
vec3 diffuse = diffuseFactor * diffuseColor * DIFFUSE_INTENSITY;
vec3 specular = specularFactor * specularColor * SPECULAR_INTENSITY;
vec3 ambient = 0.10 * diffuseColor;
return mix(diffuse + specular + ambient, specularFactor * diffuseColor, metallic);
}
A esse ponto, o specularColor pode não ter mais usos e pode ser removido.
E é assim que se parece agora:
O specular agora tem a cor do metal e ele está totalmente preto pois não tem nada para refletir.
4.2 Reflexo
Podemos agora criar uma função que faz o reflexo para nós.
vec3 computeReflection(
samplerCube cube,
vec3 viewDirection,
vec3 normal,
vec3 diffuseColor,
float metallic
) {
vec3 reflectedColor = texture(cube, reflect(viewDirection, normal)).rgb;
return mix(vec3(0.0), reflectedColor * diffuseColor, metallic);
}
Se você já sabe os básicos de cubemaps (e se você não sabe, você deveria ir aprender (em inglês)) isso não é nada novo, estamos fazendo o sampling do cubemap pelo reflexo do viewDirection com o normal da superfície e returnando uma mistura (função mix) entre zero (pois não temos reflexos para não metais ainda) e a cor do reflexo multiplicada pela cor do diffuse usando o valor do metallic.
Nós então adicionamos para a nossa saída final.
output += computeReflection(reflection, viewDirection, normal, color.rgb, metallic);
E é isso que nós temos.
E se parece com um metal agora, mas tem mais um problema, o cobre era pra ser bem áspero na verdade (roughness alto) e ele parece brilhante pois não estamos levando a roughness em conta.
4.3 O Problema da Roughness
O problema é que a roughness em um cubemap iria precisar de um cubemap filtrado como um dos usados em IBL de Specular (em inglês) e isso iria levar um tempo para fazer, e se nós vamos usar técnicas de PBR, por quê usar Blinn Phong mesmo? deve ter uma alternativa mais fácil.
4.4 A Alternativa Fácil
Bom, se não podemos filtrar o cubemap, podemos pelo menos tentar falsificar uma roughness diminuindo a resolução dos cubemaps com os mipmaps, que é bem fácil, nós só precisamos calcular a quantidade de mipmaps e então multiplicar pela roughness.
vec3 computeReflection(
samplerCube cube,
vec3 viewDirection,
vec3 normal,
vec3 diffuseColor,
float metallic,
float roughness
) {
ivec2 cubemapSize = textureSize(cube, 0);
float mipLevels = 1.0 + floor(log2(max(float(cubemapSize.x), float(cubemapSize.y))));
float lodLevel = mipLevels * roughness;
vec3 reflectedColor = textureLod(cube, reflect(viewDirection, normal), lodLevel).rgb;
return mix(vec3(0.0), reflectedColor * diffuseColor, metallic);
}
Também, lembre-se de ativar cubemaps seamless:
glEnable(GL_TEXTURE_CUBE_MAP_SEAMLESS);
E ainda se parece bem brilhante, nós podemos deixar os reflexos mais ásperos pegando a raiz quadrada da roughness.
float lodLevel = mipLevels * sqrt(roughness);
E é assim que se parece agora:
Está melhor agora.
4.5 Código Final
E esse é o código final, com o specularColor já removido da função da luz, já que já temos o valor do metallic para mudar a cor do specular.
vec3 directionalLight(
vec3 lightDirection,
vec3 viewDirection,
vec3 normal,
vec3 diffuseColor,
float metallic,
float roughness
) {
float shininess = pow(MAX_SHININESS, 1.0 - roughness);
float normalization = ((shininess + 2.0) * (shininess + 4.0)) / (8.0 * PI * (pow(2.0, -shininess * 0.5) + shininess));
normalization = max(normalization - 0.3496155267919281, 0.0) * PI;
vec3 halfwayDirection = -normalize(lightDirection + viewDirection);
float diffuseFactor = max(dot(normal, -lightDirection), 0.0);
float specularFactor = pow(max(dot(normal, halfwayDirection), 0.0), shininess) * diffuseFactor * normalization;
vec3 diffuse = diffuseFactor * diffuseColor * DIFFUSE_INTENSITY;
vec3 specular = vec3(specularFactor) * SPECULAR_INTENSITY;
vec3 ambient = 0.10 * diffuseColor;
return mix(diffuse + specular + ambient, specularFactor * diffuseColor, metallic);
}
vec3 computeReflection(
samplerCube cube,
vec3 viewDirection,
vec3 normal,
vec3 diffuseColor,
float metallic,
float roughness
) {
ivec2 cubemapSize = textureSize(cube, 0);
float mipLevels = 1.0 + floor(log2(max(float(cubemapSize.x), float(cubemapSize.y))));
float lodLevel = mipLevels * sqrt(roughness);
vec3 reflectedColor = textureLod(cube, reflect(viewDirection, normal), lodLevel).rgb;
return mix(vec3(0.0), reflectedColor * diffuseColor, metallic);
}
5 Resultados Finais
Esses são os resultados finais, todas essas texturas vem de CC0 Textures (em inglês)