Blinn Phong with Roughness Textures

This article explains a method for using roughness textures with blinn phong.

0001

2024/09/28 15:33, Updated at 2024/09/29 22:46

EN-US | PT-BR

1 Introduction

So, you finished your OpenGL blinn phong tutorial and now you rush to google (or whatever search engine you are using) in search of good looking 4K textures to test it, you end up in websites such as CC0 Textures and Ambient CG you find a good texture and download it, you look at the textures and find what you expected: a color, a normal, a height but there's one you don't know exactly what it is, a texture called roughness, you open it up and it looks like a specular texture, so you load it as a specular, but wait, what shininess value should I use? there's nothing on the description of the texture or anything, so you try a random shininess based on the texture type (high for tiles, low for rocks...) and it doesn't work well, you now go back to google and find out that for more than a decade everyone was now using PBR textures and that you may need to learn a much more complicated way to render things and you end up getting frustated as what you have learned may have become worthless today.

This is why this article was made, to show you how to adapt a old blinn phong rendering system to a roughness texture, without the need to change material properties (I won't be using materials at all), the adaptation is very simple and the results are very good for something that was made in the 70s.

This was based on this very old article: Energy Conservation In Games

2 "Fixing" Blinn Phong Specular

One of the things you may or may not have noticed is that when using low shininess values, blinn phong may generate a cone shaped specular at the dark side of a mesh, this is a not a problem with blinn phong itself but most tutorials don't talk about it.

Cone shaped specular

To fix it simply multiply the specular factor by the diffuse factor.

  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 A Directional Light

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

This is how a basic directional light usually looks like, we have the light direction, the view direction (the direction from the camera to the fragment normalize(position - camera)) the normal of the surface and the diffuseColor and specularColor from materials/textures and a shininess value from the material, for simplicity I removed the light colors vectors.

I won't talk about point lights or spot lights here, as a point light is a directional light constrained by distance and a spot light is a point light constrained by a angle.

This is how it looks on this leather texture only with color and normal textures, with 64.0 of shininess.

Directional light on leather

4 Shininess value from a roughness value

Calculating a shininess value from a roughness value is very easy, we can use a pow function and a maximum shininess value, 2048 is good for most purposes, you can put it in your material if you want too.

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

And now it looks worse than before

Worse than before

This is because we also need the specular intensity to vary with the shininess.

5 Specular intensity from a shininess value

This wasn't made by me, you should check this pdf for further details.

  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. //and multiply the specular factor by it.
  4. ...
  5. float specularFactor = pow(max(dot(normal, halfwayDirection), 0.0), shininess) * diffuseFactor * normalization;

And this is how it looks now.

Still looks bad

It still looks kind of bad, this is because we need to do one more thing.

6 Specular and diffuse material colors must sum to 1 at maximum

This is to remain "energy conserving", it can easily be done on the shader by multiplying the light's diffuse and specular factors by a constant, if you want more specular, you will need to decrease the diffuse and vice versa, I usually use a very low specular value of 5%, but you can increase if you want (remember to decrease the diffuse!), if you need more control you can place it in your material too.

  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;

And this is how it looks now, with a very faint specular, it is much better now.

Final result

This is the final code.

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

If you are rendering multiple lights on the same shader, it is better to calculate the shininess and normalization once and pass them as function arguments.

7 Results

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

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

And without configuring any material! using only three textures (color, normal and roughness).