Blinn Phong with the Fresnel Effect

Follow up of the Metallic Article, now with the Fresnel Effect.

0004

2024/10/08 14:46

EN-US | PT-BR

1 Introduction

This is a follow up of Blinn Phong with Metallic Textures and hopefully the last one, I have to say, I had never written articles before and english is not even my first language, I know that some things here may be a little bit confusing or weird, but in my opinion it is better to write a bad article about something interesting that you discovered than say it on the discord chat and let it get lost forever.

2 Fresnel Effect

The fresnel effect is one of those things that once you start noticing it exists you will always see it everywhere, from shiny plastics to ceramic tiles or large bodies of water, fresnel is usually not very visible on metals and on 3D rendering it is often assumed that metals have no fresnel at all.

Instead of showing you a boring graph, I will show you real pictures of the effect.

Low Reflection Medium Reflection High Reflection

Did you get it? if not I will say it then:

The fresnel effect is a light effect that you often see on non metallic surfaces where the reflection becomes more intense the larger the angle between the surface normal and the view direction is, as you can see in the first picture, we almost can't see a reflection, but there is still one, fresnel doesn't completely eliminates reflections when looking from the top, this is usually assumed to be close to 5% for most things but you can find tables with more values on the internet, and as you can see on the second and third picture, the reflection gets more intense as the angle increases.

3 In code

If you know what a dot product is, you probably already guessed how this is going to look.

  1. #define FRESNEL_MIN 0.05
  2. #define FRESNEL_MAX 0.95
  3. float fresnelFactor(vec3 viewDirection, vec3 normal) {
  4. float fresnelDot = 1.0 - max(dot(-viewDirection, normal), 0.0);
  5. return FRESNEL_MIN + (FRESNEL_MAX * pow(fresnelDot, 5.0));
  6. }

Here we created a function for calculating the fresnel factor for a viewDirection (in my setup, this points from the camera to the fragment, I find it more intuitive this way) and the normal from the surface.

We then calcule the dot product between the opposite of the viewDirection and the normal of the surface (this gives how much "close" the viewDirection is from the normal) and we then clamp it so it doesn't go negative and flip it because we want a value that gets larger as the angle increases.

Because fresnel is not linear (we only see very strong reflections when the angle is pretty large), we need to apply a pow function to it, which is usually 5.0, we then multiply by the max value of the fresnel effect and add the minimum value.

As the min and max always sum to one, it can be simplified to a single constant.

  1. #define FRESNEL_BALANCE 0.05
  2. float fresnelFactor(vec3 viewDirection, vec3 normal) {
  3. float fresnelDot = 1.0 - max(dot(-viewDirection, normal), 0.0);
  4. return FRESNEL_BALANCE + ((1.0 - FRESNEL_BALANCE) * pow(fresnelDot, 5.0));
  5. }

Here we call it the "FRESNEL_BALANCE" but it's normally called "F0" (Fresnel at angle 0).

Now we just need to add it to our reflection function and also multiply by the roughness so the reflection goes down as the roughness increases.

  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 * sqrt(roughness);
  12. vec3 reflectedColor = textureLod(cube, reflect(viewDirection, normal), lodLevel).rgb;
  13. return mix(
  14. reflectedColor * fresnelFactor(viewDirection, normal) * pow(1.0 - roughness, 2.0),
  15. reflectedColor * diffuseColor,
  16. metallic
  17. );
  18. }

And this is how it looks:

Red Shiny Plastic

4 Blinn Phong

After further thinking about what I have done, I eventually simplified all of the roughness to blinn phong shininess conversion into a single function that converts a PBR Metallic Roughness Material to a Blinn Phong Material in the shader, you will just need to use this as the material in your light calculations.

  1. #define PI 3.14159265359
  2. #define MAX_SHININESS 2048.0
  3. #define DIFFUSE_BALANCE 0.50
  4. #define FRESNEL_BALANCE 0.05
  5. struct BlinnPhongMaterial {
  6. float shininess;
  7. vec3 diffuse;
  8. vec3 specular;
  9. vec3 ambient;
  10. };
  11. float fresnelFactor(vec3 viewDirection, vec3 normal) {
  12. float fresnelDot = 1.0 - max(dot(-viewDirection, normal), 0.0);
  13. return FRESNEL_BALANCE + ((1.0 - FRESNEL_BALANCE) * pow(fresnelDot, 5.0));
  14. }
  15. BlinnPhongMaterial convertPBRMaterialToBlinnPhong(
  16. vec3 viewDirection, vec3 normal,
  17. vec3 color, float metallic, float roughness, float ambientOcclusion
  18. ) {
  19. float shininess = pow(MAX_SHININESS, 1.0 - roughness);
  20. float specular = ((shininess + 2.0) * (shininess + 4.0)) / (8.0 * PI * (pow(2.0, -shininess * 0.5) + shininess));
  21. float fresnel = fresnelFactor(viewDirection, normal);
  22. return BlinnPhongMaterial(
  23. shininess,
  24. mix(
  25. (color * DIFFUSE_BALANCE) / PI,
  26. vec3(0.0),
  27. metallic
  28. ),
  29. mix(
  30. vec3(max(specular - 0.3496155267919281, 0.0)) * (1.0 - DIFFUSE_BALANCE) * fresnel * PI,
  31. vec3(specular) * color,
  32. metallic
  33. ),
  34. mix(
  35. vec3(ambientOcclusion) * color,
  36. vec3(0.0),
  37. metallic
  38. )
  39. );
  40. }
  1. vec3 directionalLight(
  2. vec3 lightDirection,
  3. vec3 viewDirection,
  4. vec3 normal,
  5. BlinnPhongMaterial material
  6. ) {
  7. vec3 halfwayDirection = -normalize(lightDirection + viewDirection);
  8. float diffuseFactor = max(dot(normal, -lightDirection), 0.0);
  9. float specularFactor = pow(max(dot(normal, halfwayDirection), 0.0), material.shininess) * diffuseFactor;
  10. float lightDiffuse = 1.90;
  11. float lightSpecular = 1.0;
  12. float lightAmbient = 0.10;
  13. vec3 diffuse = lightDiffuse * diffuseFactor * material.diffuse;
  14. vec3 specular = lightSpecular * specularFactor * material.specular;
  15. vec3 ambient = lightAmbient * material.ambient;
  16. return diffuse + specular + ambient;
  17. }

This time I divided the diffuse by PI so it will look darker, I also added a fresnel to it, so it should look more realistic when looking from high angles, this is how it looks on the leather texture with no reflections:

Leather

I also split the reflection function into three more generic functions.

  1. float cubemapMipLevels(samplerCube cube) {
  2. ivec2 cubemapSize = textureSize(cube, 0);
  3. float mipLevels = 1.0 + floor(log2(max(float(cubemapSize.x), float(cubemapSize.y))));
  4. return mipLevels;
  5. }
  6. vec4 cubemapRoughnessColor(samplerCube cube, float roughness, vec3 direction) {
  7. float mipLevels = cubemapMipLevels(cube);
  8. float lodLevel = mipLevels * sqrt(roughness);
  9. return textureLod(cube, direction, lodLevel);
  10. }
  11. vec3 cubemapReflection(
  12. vec3 cubemapColor,
  13. vec3 viewDirection, vec3 normal,
  14. vec3 color, float metallic, float roughness
  15. ) {
  16. return mix(
  17. cubemapColor * fresnelFactor(viewDirection, normal) * pow(1.0 - roughness, 2.0),
  18. cubemapColor * color,
  19. metallic
  20. );
  21. }

5 Final Results

This time those textures come from multiple sources.

Monitor Ice Gold Marble Wood Sand with water