Subsurface scattering (also known as subsurface light transport) happens when photons penetrate a translucent surface and scatters within the material until they exit at a different location.

Image courtesy of Mickey Mouse

A common technique in rendering that can be used to render subsurface scattering is called depth map based SSS. In this technique, a depth map is generated from the light point of view, similarly to the shadow mapping technique. Then, the thickness of the model at a given fragment can be easily retrieved by sampling the depth map and comparing it to the current fragment depth.
While this technique gives good results, choosing a depth bias can become cumbersome, and convex models might show some artifacts.

As an experiment and an alternative to the depth map based SSS, I wanted to give another technique a try using a 3d distance field that, once generated, can be found particularly useful for a handful of other rendering effect.

In the first step of the algorithm, a distance field needs to be generated and stored in a 3d texture. Here is a view of the different slices of the 3d texture distance field of the famous 3d model suzanne:

Then, when rendering the triangle mesh, each vertex is transformed in UV space. Transforming a vertex in UV space can be done as such:

vec3 toUVSpace(vec3 position) {
    return (position + abs(u_min)) / (u_max - u_min);

Where u_min, and u_max are the extent of the bounding box containing the 3d mesh.

The next step is to step through the texture cube starting from the current fragment UV. The direction used to step through the 3d texture cube is given by a view ray (normalized vector from the view to the fragment interpolated position). For each iteration through the 3d texture along the ray, accumulate the thickness by ray marching in the light direction. To get the light direction, the UV needs to be transformed in view space.

The distance field when sampled from the texture, has the following meaning:

  • if sampledValue < 0.5: inside the triangle mesh
  • if sampledValue > 0.5: outside the triangle mesh

Positive distance (outside the mesh) will have no contribution to the thickness while negative distance (inside the mesh) in the field will contribute, so the following can be used:

sdf = texture(u_texture, uv).r;
sdf = clamp(sdf, 0.0, 0.5) * 2.0;
thickness += (1.0 - sdf) / steps;

Some results of the final render:

Note that generating 3d distance fields will result in a higher memory footprint than the depth based technique.

References and further reading