Blinn Phong com Textures de Metallic

Seguimento do Artigo de Roughness, agora com Texturas Metallic.

0003

2024/10/04 01:24

EN-US | PT-BR

1 Introdução

Isso é o seguimento de Blinn Phong with Roughness Textures

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.

Washed Color

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.

  1. float normalization = ((shininess + 2.0) * (shininess + 4.0)) / (8.0 * PI * (pow(2.0, -shininess * 0.5) + shininess));
  2. 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).

Pitch Black

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.

  1. float normalization = ((shininess + 2.0) * (shininess + 4.0)) / (8.0 * PI * (pow(2.0, -shininess * 0.5) + shininess));
  2. normalization = max(normalization - 0.3496155267919281, 0.0) * PI;

É assim que se parece agora, com um especular mais forte.

Strong Specular

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.

  1. 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.

Mirror

Olhando um espelho nós vemos duas coisas:

Agora, vamos olhar outro exemplo (isso é de um doce, mas tem as propriedades de um metal):

Yellow Metal

E é isso que nós vemos:

Então, dessas pequenas observações nós vemos que:

4 Em código

É assim que essa textura de cobre atualmente se parece.

Plastic Copper

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

  1. diffuseFactor *= DIFFUSE_INTENSITY;
  2. specularFactor *= SPECULAR_INTENSITY;
  3. vec3 diffuse = diffuseFactor * diffuseColor;
  4. vec3 specular = specularFactor * specularColor;
  5. vec3 ambient = 0.10 * diffuseColor;
  6. return diffuse + specular + ambient;

Se torna isso

  1. vec3 diffuse = diffuseFactor * diffuseColor * DIFFUSE_INTENSITY;
  2. vec3 specular = specularFactor * specularColor * SPECULAR_INTENSITY;
  3. vec3 ambient = 0.10 * diffuseColor;
  4. return mix(diffuse + specular + ambient, specularFactor * diffuseColor, metallic);

E o código completo é:

  1. vec3 directionalLight(
  2. vec3 lightDirection, vec3 viewDirection, vec3 normal,
  3. vec3 diffuseColor, vec3 specularColor,
  4. float roughness, float metallic
  5. ) {
  6. float shininess = pow(MAX_SHININESS, 1.0 - roughness);
  7. float normalization = ((shininess + 2.0) * (shininess + 4.0)) / (8.0 * PI * (pow(2.0, -shininess * 0.5) + shininess));
  8. normalization = max(normalization - 0.3496155267919281, 0.0) * PI;
  9. vec3 halfwayDirection = -normalize(lightDirection + viewDirection);
  10. float diffuseFactor = max(dot(normal, -lightDirection), 0.0);
  11. float specularFactor = pow(max(dot(normal, halfwayDirection), 0.0), shininess) * diffuseFactor * normalization;
  12. vec3 diffuse = diffuseFactor * diffuseColor * DIFFUSE_INTENSITY;
  13. vec3 specular = specularFactor * specularColor * SPECULAR_INTENSITY;
  14. vec3 ambient = 0.10 * diffuseColor;
  15. return mix(diffuse + specular + ambient, specularFactor * diffuseColor, metallic);
  16. }

A esse ponto, o specularColor pode não ter mais usos e pode ser removido.

E é assim que se parece agora:

Dark Copper

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.

  1. vec3 computeReflection(
  2. samplerCube cube,
  3. vec3 viewDirection,
  4. vec3 normal,
  5. vec3 diffuseColor,
  6. float metallic
  7. ) {
  8. vec3 reflectedColor = texture(cube, reflect(viewDirection, normal)).rgb;
  9. return mix(vec3(0.0), reflectedColor * diffuseColor, metallic);
  10. }

Se você já sabe os báximos 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.

  1. output += computeReflection(reflection, viewDirection, normal, color.rgb, metallic);

E é isso que nós temos.

Shiny Copper

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 Alternative 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.

  1. vec3 computeReflection(
  2. samplerCube cube,
  3. vec3 viewDirection,
  4. vec3 normal,
  5. vec3 diffuseColor,
  6. float metallic,
  7. float roughness
  8. ) {
  9. ivec2 cubemapSize = textureSize(cube, 0);
  10. float mipLevels = 1.0 + floor(log2(max(float(cubemapSize.x), float(cubemapSize.y))));
  11. float lodLevel = mipLevels * roughness;
  12. vec3 reflectedColor = textureLod(cube, reflect(viewDirection, normal), lodLevel).rgb;
  13. return mix(vec3(0.0), reflectedColor * diffuseColor, metallic);
  14. }

Também, lembre-se de ativar cubemaps seamless:

  1. glEnable(GL_TEXTURE_CUBE_MAP_SEAMLESS);
Quite Shiny Copper

E ainda se parece bem brilhante, nós podemos deixar os reflexos mais ásperos pegando a raiz quadrada da roughness.

  1. float lodLevel = mipLevels * sqrt(roughness);

E é assim que se parece agora:

Quite Rough Copper

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.

  1. vec3 directionalLight(
  2. vec3 lightDirection,
  3. vec3 viewDirection,
  4. vec3 normal,
  5. vec3 diffuseColor,
  6. float metallic,
  7. float roughness
  8. ) {
  9. float shininess = pow(MAX_SHININESS, 1.0 - roughness);
  10. float normalization = ((shininess + 2.0) * (shininess + 4.0)) / (8.0 * PI * (pow(2.0, -shininess * 0.5) + shininess));
  11. normalization = max(normalization - 0.3496155267919281, 0.0) * PI;
  12. vec3 halfwayDirection = -normalize(lightDirection + viewDirection);
  13. float diffuseFactor = max(dot(normal, -lightDirection), 0.0);
  14. float specularFactor = pow(max(dot(normal, halfwayDirection), 0.0), shininess) * diffuseFactor * normalization;
  15. vec3 diffuse = diffuseFactor * diffuseColor * DIFFUSE_INTENSITY;
  16. vec3 specular = vec3(specularFactor) * SPECULAR_INTENSITY;
  17. vec3 ambient = 0.10 * diffuseColor;
  18. return mix(diffuse + specular + ambient, specularFactor * diffuseColor, metallic);
  19. }
  20. vec3 computeReflection(
  21. samplerCube cube,
  22. vec3 viewDirection,
  23. vec3 normal,
  24. vec3 diffuseColor,
  25. float metallic,
  26. float roughness
  27. ) {
  28. ivec2 cubemapSize = textureSize(cube, 0);
  29. float mipLevels = 1.0 + floor(log2(max(float(cubemapSize.x), float(cubemapSize.y))));
  30. float lodLevel = mipLevels * sqrt(roughness);
  31. vec3 reflectedColor = textureLod(cube, reflect(viewDirection, normal), lodLevel).rgb;
  32. return mix(vec3(0.0), reflectedColor * diffuseColor, metallic);
  33. }

5 Resultados Finais

Esses são os resultados finais, todas essas texturas vem de CC0 Textures (em inglês)

Final Steel Final Rusty Steel Final Gold Final Copper Final Brass Final Brushed Steel