Blinn Phong com Texturas de Roughness

Esse artigo explica um método para utilizar texturas de roughness com blinn phong.

0001

2024/09/28 15:33, Atualizado em 2024/09/29 22:46

EN-US | PT-BR

1 Introdução

Então, você terminou seu tutorial de blinn phong no OpenGL e agora você corre para o google (ou qualquer que seja o motor de busca que esteja usando) em procura de boas texturas em 4K para testar, você termina em sites como CC0 Textures e Ambient CG você encontra uma boa textura e baixa ela, você olha as texturas e encontra o que esperava: uma cor, um normal, um height mas tem uma que você não sabe exatamente o que é, uma textura chamada roughness, você abre ela e ela se parece com uma textura de specular então você carrega ela como uma de specular, mas espera, que valor de shininess eu deveria usar? não tem nada na descrição da textura ou em outros lugares, então você tenta uma shininess aleatória baseada no tipo da textura (alta para pisos, baixa para rochas...) e não funciona bem, então você volta para o google e descobre que por mais de uma década todo mundo agora estava usando texturas de PBR e que agora você talvez precise aprender um jeito muito mais complicado de renderizar coisas e você termina ficando frustado já que o que aprendeu pode ter se tornado inútil nos dias de hoje.

É por isso que esse artigo foi feito, para mostrar a você como adaptar um velho sistema de blinn phong para uma textura de roughness, sem a necessidade de mudar as propriedades dos materiais (nem usarei materiais na verdade), a adaptação é bem simples e os resultados são ótimos para algo que foi feito nos anos 70.

Isso foi baseado nesse artigo bem antigo (em inglês): Energy Conservation In Games

2 "Arrumando" o Specular do Blinn Phong

Uma das coisas que você pode ou não pode ter percebido é que quando valores baixos de shininess são utilizados, blinn phong pode gerar um specular em formato de cone no lado escuro de uma mesh, isso não é um problema com blinn phong em si mas grande parte dos tutoriais não falam sobre ele.

Cone shaped specular

Para arrumar simplesmente multiplique o fator de specular pelo fator de diffuse.

  1. vec3 halfwayDirection = -normalize(lightDirection + viewDirection);
  2. float diffuseFactor = max(dot(normal, -lightDirection), 0.0);
  3. float specularFactor = pow(max(dot(normal, halfwayDirection), 0.0), shininess) * diffuseFactor;
Fixed cone shaped specular

3 Uma Luz Direcional

  1. vec3 directionalLight(
  2. vec3 lightDirection, vec3 viewDirection, vec3 normal,
  3. vec3 diffuseColor, vec3 specularColor, float shininess
  4. ) {
  5. vec3 halfwayDirection = -normalize(lightDirection + viewDirection);
  6. float diffuseFactor = max(dot(normal, -lightDirection), 0.0);
  7. float specularFactor = pow(max(dot(normal, halfwayDirection), 0.0), shininess) * diffuseFactor;
  8. vec3 diffuse = diffuseFactor * diffuseColor;
  9. vec3 specular = specularFactor * specularColor;
  10. vec3 ambient = 0.10 * diffuseColor;
  11. return diffuse + specular + ambient;
  12. }

É assim que uma luz direcional básica normalmente se parece, nós temos a direção da luz (lightDirection), a direção de view (viewDirection) (a direção da câmera para o fragment normalize(position - camera)) o normal da superfície e o diffuseColor e o specularColor dos materiais/texturas e um valor de shininess do material, para manter a simplicidade, eu removi os vetores de cores da luz.

Não vou falar sobre point lights ou spot lights aqui, já que uma point light é uma luz direcional restringida por uma distância e uma spot light é uma point light restringida por um ângulo.

É assim que se parece nessa textura de couro somente com as texturas de cor e normal, com 64.0 de shininess.

Directional light on leather

4 Valor de shininess de um valor de roughness

Calcular um valor de shininess a partir de um valor de roughness é bem fácil, podemos usar uma função de pow e um valor máximo de shininess, 2048 é bom para a maioria dos casos, você pode colocar isso no seu material se quiser também.

  1. #define MAX_SHININESS 2048.0
  2. float shininess = pow(MAX_SHININESS, 1.0 - roughness);

E agora está pior do que antes.

Worse than before

Isso é porque também precisamos que a intensidade do specular varie com a shininess.

5 Intensidade de specular de um valor de shininess

Isso não foi feito por mim, você deveria checar esse pdf (em inglês) para mais detalhes.

  1. #define PI 3.14159265359
  2. float normalization = ((shininess + 2.0) * (shininess + 4.0)) / (8.0 * PI * (pow(2.0, -shininess * 0.5) + shininess));
  3. //e multiplique o fator de specular por ele
  4. ...
  5. float specularFactor = pow(max(dot(normal, halfwayDirection), 0.0), shininess) * diffuseFactor * normalization;

E é assim que se parece agora.

Still looks bad

Ainda está um pouco ruim, isso é porque precisamos fazer mais uma coisa.

6 Specular e diffuse dos materiais deve somar para 1 no máximo

Isso é para manter a "conservação de energia", pode ser facilmente fefito no shader multiplicando os fatores de diffuse e specular da luz por uma constante, se você quer mais specular, você terá que diminuir o diffuse e vice versa, eu normalmente uso um valor bem baixo de specular de 5%, mas você pode aumentar se quiser (lembre-se de diminuir o diffuse!), se você quer mais controle pode colocar no seu material também.

  1. #define DIFFUSE_INTENSITY 0.95
  2. #define SPECULAR_INTENSITY 0.05
  3. float diffuseFactor = ...
  4. float specularFactor = ...
  5. diffuseFactor *= DIFFUSE_INTENSITY;
  6. specularFactor *= SPECULAR_INTENSITY;

E é assim que se parece agora, com um specular bem fraco, está muito melhor agora.

Final result

Esse é o código final.

  1. #define PI 3.14159265359
  2. #define MAX_SHININESS 2048.0
  3. #define DIFFUSE_INTENSITY 0.95
  4. #define SPECULAR_INTENSITY 0.05
  5. vec3 directionalLight(
  6. vec3 lightDirection, vec3 viewDirection, vec3 normal,
  7. vec3 diffuseColor, vec3 specularColor, 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. vec3 halfwayDirection = -normalize(lightDirection + viewDirection);
  12. float diffuseFactor = max(dot(normal, -lightDirection), 0.0);
  13. float specularFactor = pow(max(dot(normal, halfwayDirection), 0.0), shininess) * diffuseFactor * normalization;
  14. diffuseFactor *= DIFFUSE_INTENSITY;
  15. specularFactor *= SPECULAR_INTENSITY;
  16. vec3 diffuse = diffuseFactor * diffuseColor;
  17. vec3 specular = specularFactor * specularColor;
  18. vec3 ambient = 0.10 * diffuseColor;
  19. return diffuse + specular + ambient;
  20. }

Se você está renderizando múltiplas luzes no mesmo shader, é melhor calcular o shininess e normalization apenas uma vez e passar eles como argumentos de funções.

7 Resultados

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

final bricks final leather final rock final stone tiles final tiles final wood

E sem configurar nenhum material! usando apenas três texturas (cor, normal e roughness).