Blinn Phong with Metallic Textures

Follow up of the Roughness Article, now with Metallic Textures.

0003

2024/10/04 01:24

EN-US | PT-BR

1 Introduction

This is a follow up of Blinn Phong with Roughness Textures

1.1 Washed colors

In very dark and very rough textures there can still be a very small specular causing the texture to look washed out, this can be undesirable if the texture was not supposed to have specular at all, such as doom style sprites or half life 1 textures.

Washed Color

What we can do is to calculate the normalization for the smaller shininess we can have, which is 1.0, and then use it to shift the normalization.

  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);

The max function is to avoid the normalization from going negative and causing undefined behaviour (due to floating point imprecision).

Pitch Black

The texture is now pitch-black as it was supposed to be.

1.2 Diffuse Energy Conservation

From Energy Conservation In Games

The author suggests dividing the material diffuse color by PI but this would cause the scene to become darker which would require the light to be multiplied by PI in order for the diffuse to remain as bright as it was.

Instead of doing this, we can simply multiply the normalization by PI, which would be the same as dividing the diffuse by PI and increasing the light intensity by 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;

This is how it looks now, with a stronger specular.

Strong Specular

2 Metallic Texture

So, a metallic (or metalness) texture defines if the surface is a metal or not, but because it usually goes from 0 to 255 or 0 to 1 (if normalized) and because there's no such thing as a "half metal", it in practice defines a linear interpolation between a non metallic version of the material and a metallic version of the material.

  1. vec3 outputColor = mix(nonMetallicOutputColor, metallicOutputColor, metallic);

3 Metals

So, what does make a metal look like a metal? let's look at the most metallic and shiny thing everyone owns, a mirror.

Mirror

Looking at a mirror we see two things:

Now, let's look at another example (this is from a candy bar, but it has the properties of a metal):

Yellow Metal

And this is what we see:

So, from those small observations we see that:

4 In code

This is how this copper texture currently looks like.

Plastic Copper

It does not look like a metal at all, it looks like plastic.

4.1 Lighting

So, what we need to do is actually quite simple, we only need to multiply the specularFactor by the diffuseColor and mix it with the current sum of light components using the metallic value, we also must remember to move both DIFFUSE_INTENSITY and SPECULAR_INTENSITY to the diffuse and specular light calculation as a metal is 0.0 diffuse and 1.0 specular (the specular is a reflection of the light).

So this

  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;

Becomes this

  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);

And the full code is:

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

At this point, the specularColor may have no more uses and can be removed.

And this is how it looks now:

Dark Copper

The specular now has the color of the metal and it looks fully black because it has nothing to reflect.

4.2 Reflection

We can now create a function that does the reflection for us.

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

If you already know the basics of cubemaps (and if you don't, you should go learn it) this is nothing new, we are sampling the cubemap by the reflection of the viewDirection with the normal of the surface and returning a mix between zero (because we don't have reflections for non metals yet) and the reflection color multiplied by the diffuse color using the metallic value.

We then add it to our final output color.

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

And this is what we get.

Shiny Copper

And it looks like a metal now, but there's one more problem, the copper was supposed to be quite rough actually and it looks shiny because we are not taking the roughness into account.

4.3 The Roughness Problem

The issue is that roughness on a cubemap would require a filtered cubemap such as one from a Specular IBL and this would take some time to do, and if we are going to use PBR techniques, why use Blinn Phong after all? there has to be some easy alternative.

4.4 The Easy Alternative

Well, if we can't filter the cubemap, we can at least try to fake roughness by decreasing the resolution of the cubemap using the mipmaps, which is quite easy, we only need to calculate the amount of mipmaps and then multiply it by the 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. }

Also, remember to enable seamless cubemaps:

  1. glEnable(GL_TEXTURE_CUBE_MAP_SEAMLESS);
Quite Shiny Copper

And it still looks quite shiny, we can make the reflections more rough by taking the square root of the roughness.

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

And this is how it looks:

Quite Rough Copper

It is better now.

4.5 Final Code

And this is the final code, with the specularColor already removed from the light function, as we already have the metallic value for changing the color of the 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 Final Results

Those are the final results, all of these textures come from CC0 Textures

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