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.
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.
float normalization = ((shininess + 2.0) * (shininess + 4.0)) / (8.0 * PI * (pow(2.0, -shininess * 0.5) + shininess));
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).
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.
float normalization = ((shininess + 2.0) * (shininess + 4.0)) / (8.0 * PI * (pow(2.0, -shininess * 0.5) + shininess));
normalization = max(normalization - 0.3496155267919281, 0.0) * PI;
This is how it looks now, with a stronger 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.
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.
Looking at a mirror we see two things:
It is fully reflective.
It has no diffuse or shadows at all (look at the shadows in the wood surface).
Now, let's look at another example (this is from a candy bar, but it has the properties of a metal):
And this is what we see:
It has a little bit of diffuse, so it's not fully metallic.
The color of the reflection seems to have the same color as it's surface.
So, from those small observations we see that:
Metals are fully reflective.
Metals have almost no diffuse at all.
The color of the reflection has the same color as the metal.
4 In code
This is how this copper texture currently looks like.
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
diffuseFactor *= DIFFUSE_INTENSITY;
specularFactor *= SPECULAR_INTENSITY;
vec3 diffuse = diffuseFactor * diffuseColor;
vec3 specular = specularFactor * specularColor;
vec3 ambient = 0.10 * diffuseColor;
return diffuse + specular + ambient;
Becomes this
vec3 diffuse = diffuseFactor * diffuseColor * DIFFUSE_INTENSITY;
vec3 specular = specularFactor * specularColor * SPECULAR_INTENSITY;
vec3 ambient = 0.10 * diffuseColor;
return mix(diffuse + specular + ambient, specularFactor * diffuseColor, metallic);
And the full code is:
vec3 directionalLight(
vec3 lightDirection, vec3 viewDirection, vec3 normal,
vec3 diffuseColor, vec3 specularColor,
float roughness, float metallic
) {
float shininess = pow(MAX_SHININESS, 1.0 - roughness);
float normalization = ((shininess + 2.0) * (shininess + 4.0)) / (8.0 * PI * (pow(2.0, -shininess * 0.5) + shininess));
normalization = max(normalization - 0.3496155267919281, 0.0) * PI;
vec3 halfwayDirection = -normalize(lightDirection + viewDirection);
float diffuseFactor = max(dot(normal, -lightDirection), 0.0);
float specularFactor = pow(max(dot(normal, halfwayDirection), 0.0), shininess) * diffuseFactor * normalization;
vec3 diffuse = diffuseFactor * diffuseColor * DIFFUSE_INTENSITY;
vec3 specular = specularFactor * specularColor * SPECULAR_INTENSITY;
vec3 ambient = 0.10 * diffuseColor;
return mix(diffuse + specular + ambient, specularFactor * diffuseColor, metallic);
}
At this point, the specularColor may have no more uses and can be removed.
And this is how it looks now:
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.
vec3 computeReflection(
samplerCube cube,
vec3 viewDirection,
vec3 normal,
vec3 diffuseColor,
float metallic
) {
vec3 reflectedColor = texture(cube, reflect(viewDirection, normal)).rgb;
return mix(vec3(0.0), reflectedColor * diffuseColor, metallic);
}
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.
output += computeReflection(reflection, viewDirection, normal, color.rgb, metallic);
And this is what we get.
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.
vec3 computeReflection(
samplerCube cube,
vec3 viewDirection,
vec3 normal,
vec3 diffuseColor,
float metallic,
float roughness
) {
ivec2 cubemapSize = textureSize(cube, 0);
float mipLevels = 1.0 + floor(log2(max(float(cubemapSize.x), float(cubemapSize.y))));
float lodLevel = mipLevels * roughness;
vec3 reflectedColor = textureLod(cube, reflect(viewDirection, normal), lodLevel).rgb;
return mix(vec3(0.0), reflectedColor * diffuseColor, metallic);
}
Also, remember to enable seamless cubemaps:
glEnable(GL_TEXTURE_CUBE_MAP_SEAMLESS);
And it still looks quite shiny, we can make the reflections more rough by taking the square root of the roughness.
float lodLevel = mipLevels * sqrt(roughness);
And this is how it looks:
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.
vec3 directionalLight(
vec3 lightDirection,
vec3 viewDirection,
vec3 normal,
vec3 diffuseColor,
float metallic,
float roughness
) {
float shininess = pow(MAX_SHININESS, 1.0 - roughness);
float normalization = ((shininess + 2.0) * (shininess + 4.0)) / (8.0 * PI * (pow(2.0, -shininess * 0.5) + shininess));
normalization = max(normalization - 0.3496155267919281, 0.0) * PI;
vec3 halfwayDirection = -normalize(lightDirection + viewDirection);
float diffuseFactor = max(dot(normal, -lightDirection), 0.0);
float specularFactor = pow(max(dot(normal, halfwayDirection), 0.0), shininess) * diffuseFactor * normalization;
vec3 diffuse = diffuseFactor * diffuseColor * DIFFUSE_INTENSITY;
vec3 specular = vec3(specularFactor) * SPECULAR_INTENSITY;
vec3 ambient = 0.10 * diffuseColor;
return mix(diffuse + specular + ambient, specularFactor * diffuseColor, metallic);
}
vec3 computeReflection(
samplerCube cube,
vec3 viewDirection,
vec3 normal,
vec3 diffuseColor,
float metallic,
float roughness
) {
ivec2 cubemapSize = textureSize(cube, 0);
float mipLevels = 1.0 + floor(log2(max(float(cubemapSize.x), float(cubemapSize.y))));
float lodLevel = mipLevels * sqrt(roughness);
vec3 reflectedColor = textureLod(cube, reflect(viewDirection, normal), lodLevel).rgb;
return mix(vec3(0.0), reflectedColor * diffuseColor, metallic);
}
5 Final Results
Those are the final results, all of these textures come from CC0 Textures