For an ice effect I worked on recently, I used this tutorial by Alan Zucconi to learn about the implementation of subsurface scattering in Unity shaders.
The concept and mathematics are explained properly in his tutorial, but in brief, subsurface scattering is when light hits a translucent material and instead of exiting the other side like with a transparent one, it bounces around inside until it finds its way out. Therefore, light absorbed at one point is not transmitted at the same point.
This can be seen when things appear to glow slightly in real life – think skin, milk and marble.
In code, we take the dot product of the view direction and the negative light vector plus distortion amount multiplied by surface normal. We then alter this using a texture, scale and power. Essentially what we are doing is looking at light as if it had come directly out of the other side, checking how much of this the player can see due to the angle between their view direction and the vector, and distorting the vector based on some user parameters. This distortion is the fake subsurface scattering. In my example, it mostly relies on a thickness texture to allow artist control, which is shown below.
I also added ambiance and intensity controls, which Zucconi talks about in the second part of his tutorial. This is an artificial scalar of light propagation across the surface, and the overall intensity of that light.
This is all done in a function that extends the unity’s PBR translucent lighting. Code below!
inline fixed4 LightingStandardTranslucent(SurfaceOutputStandard s, fixed3 viewDir, UnityGI gi)
fixed4 pbr = LightingStandard(s, viewDir, gi); //inbuilt lighting function
//Swap out thickness value if area is not ice
thickness *= smoothstep(0, 0.1, (1 – iceAmount));
float3 lightVector = gi.light.dir;
float3 viewDirection = viewDir;
float3 surfaceNormal = s.Normal;
float3 halfPoint = normalize(lightVector + surfaceNormal * _Distortion);
float intensity = pow(saturate(dot(lightVector, -halfPoint)), _Power) * _Scale;
float subsurface = _Attenuation * (intensity + _Ambient) * thickness;
//Add to PBR
pbr.rgb += gi.light.color * subsurface;
void LightingStandardTranslucent_GI(SurfaceOutputStandard s, UnityGIInput data, inout UnityGI gi)
LightingStandard_GI(s, data, gi);
The texture below was the main texture, with each channel scrolling seperatley to create the magical under-surface effect. This looks like it is below the surface of the ice because the details are not present on the thickness map, and therefore is independent of the subsurface scattering light function.
This is the thickness map, and is largely used to say which parts of the ice are thicker than others, thus how much light exits when passing through the surface. By using the pixelate -> cells filter in photoshop, I changed a clouds render into a voronoi noise pattern. This gave me a nice faceted sort of look that I wanted for my stylized ice.
The normal map was created from the thickness map, to make sure that reflected light was consistent with transmitted light.
The alpha erosion used a cloud render with levels used to increase the value range. Here, black areas erode sooner than white areas due to a subtractive method being used to control coverage.