Lately I was able to work on materials that were used to represent human skin and I thought it would be nice to share the knowledge.
I was searching for a simple real-time GLSL code to represent human skin, and if possible without using any additional resources like gradient textures or blurred textures.
The main idea behind human skin properties is that part of the light that arrives to the skin surface is absorbed by the skin, then the light bounces inside the flesh and comes out from a different position of where it was absorbed.
While the light is bouncing inside the flesh, its color tends to turn red, this is because in this case we asume that the inner color of the flesh is red because of human blood.
I found 2 very interesting articles on internet:
On both articles we can read about the idea of extending the light "reach" by modifying the result of the diffuse term. Also the idea of making the diffuse color to turn into red (human blood color).
If we start with the Lambertian term, we will have something like this:
diffuse = dot(N, L);
Being N
the normalized normal of the pixel, and L
the normalized vector from the pixel to the light we are computing.
This function will give us this result:
And normally, we clamp the result between 1.0 and 0.0
diffuse = clamp(dot(N, L), 0.0, 1.0);
making the result look like this:
On both cases we can see that no positive value of diffuse goes beyond π/2 (or 90 degrees).
Now, instead of clamping the Lambertian term, we are going to transform it into a Half-Lambertian term, this way we would be able to extend the "reach" of the light.
Lets change our previous code to:
diffuse = (dot(N, L) * 0.5) + 0.5;
Now the function will look like this:
As we can see, the Half-Lambertian term give us positive values past π/2 (or 90 degrees), but it is too much light and a model shaded with a Half-Lambertian term can be perceived more like a cartoon rather than a realistic model.
So, to reduce the amount if light we can square the result, transforming the Half-Lambertian term into a Squared Half-Lambertian term:
diffuse = (dot(N, L) * 0.5) + 0.5;
diffuse = diffuse * diffuse;
The squared result will look approximately like this:
At this point we can see that our diffuse term will give us results not only a bit greater than a Lambertian term but also it is going to give us positive values beyond π/2 (or 90 degrees), giving us the approximation of light traveling and bouncing inside the skin and coming out at a distant point from the entrance point.
Now we need to "ink" the light that entered the skin and came out on another point, which in our Squared Lambertian term is the light that goes from π/2 to π (90 to 180 degrees).
On the GPU Gems 3 Chapter, they use the instruction smoothstep
to blend between the diffuse color and the blood color.
I tried an alternative route using the existing diffuse value to lerp between the diffuse value and the blood value.
diffuse = dot(N, L);
hDiffuse = (diffuse * 0.5) + 0.5;
shDiffuse = hDiffuse * hDiffuse;
bf = 1.0 - diffuse;
bf = clamp(bf, 0.0, 1.0);
colorMult = lerp(vec3(1.0), vec3(0.6, 0.0, 0.0), bf);
return shDiffuse * colorMult;
We need a blend factor to interpolate between white color and the blood color. In the code above we used 1.0 minus diffuse to give us a curve of interpolation between 0.0 and 1.0. After that, we clamped the result and we used that bf
variable to lerp between colors.
The result of this code will be a white color multiplied by the Squared Half-Lambertian term between 0 and π/2, and between π/2 and π we will have the rest of the Square Half-Lambertian term multiplied with a progressively more blood-ish color.
The final result will be a diffuse value with approximated properties of the skin. After that, we would want to accumulate that diffuse value with other diffuse values from other lights and multiply it with the albedo texture of that surface.