GLSL Programming/Unity/Translucent Bodies

Chinese jade figure (Han dynasty, 206 BC - AD 220). Note the almost wax-like illumination around the nostrils of the horse.

This tutorial covers translucent bodies.

It is one of several tutorials about lighting that go beyond the Phong reflection model. However, it is based on per-pixel lighting with the Phong reflection model as described in Section “Smooth Specular Highlights”. If you haven't read that tutorial yet, you should read it first.

The Phong reflection model doesn't take translucency into account, i.e. the possibility that light is transmitted through a material. While Section “Translucent Surfaces” handled translucent surfaces, this tutorial handles the case of three-dimensional bodies instead of thin surfaces. Examples of translucent materials are wax, jade, marble, skin, etc.

Wax idols. Note the reduced contrast of diffuse lighting.

WaxinessEdit

Unfortunately, the light transport in translucent bodies (i.e. subsurface scattering) is quite challenging in a real-time game engine. Rendering a depth map from the point of view of the light source would help, but since this tutorial is restricted to the free version of Unity, this approach is out of the question. Therefore, we will fake some of the effects of subsurface scattering.

The first effect will be called “waxiness” and describes the smooth, lustrous appearance of wax which lacks the hard contrasts that diffuse reflection can provide. Ideally, we would like to smooth the surface normals before we compute the diffuse reflection (but not the specular reflection) and, in fact, this is possible if a normal map is used. Here, however, we take another approach. In order to soften the hard contrasts of diffuse reflection, which is caused by the term max(0, N·L) (see Section “Diffuse Reflection”), we reduce the influence of this term as the waxiness w increases from 0 to 1. More specifically, we multiply the term max(0, N·L) with 1 - w. However, this will not only reduce the contrast but also the overall brightness of the illumination. To avoid this, we add the waxiness w to fake the additional light due to subsurface scattering, which is stronger the “waxier” a material is.

Thus, instead of this equation for diffuse reflection:

I_\text{diffuse} = I_\text{incoming}\,k_\text{diffuse} \max(0,\mathbf{N}\cdot \mathbf{L})

we get:

I_\text{diffuse} = I_\text{incoming}\,k_\text{diffuse} \left(w + (1-w) \max(0,\mathbf{N}\cdot \mathbf{L})\right)

with the waxiness w between 0 (i.e. regular diffuse reflection) and 1 (i.e. no dependency on N·L).

This approach is easy to implement, easy to compute for the GPU, easy to control, and it does resemble the appearance of wax and jade, in particular if combined with specular highlights with a high shininess.

Chessmen in backlight. Note the translucency of the white chessmen.

Transmittance of BacklightEdit

The second effect that we are going to fake is backlight that passes through a body and exits at the visible front of the body. This effect is the stronger, the smaller the distance between the back and the front, i.e. in particular at silhouettes, where the distance between the back and the front actually becomes zero. We could, therefore, use the techniques discussed in Section “Silhouette Enhancement” to generate more illumination at the silhouettes. However, the effect becomes somewhat more convincing if we take the actual diffuse illumination at the back of a closed mesh into account. To this end, we proceed as follows:

  • We render only back faces and compute the diffuse reflection weighted with a factor that describes how close the point (on the back) is to a silhouette. We mark the pixels with an opacity of 0. (Usually, pixels in the framebuffer have opacity 1. The technique of marking pixels by setting their opacity to 0 is also used and explained in more detail in Section “Mirrors”.)
  • We render only front faces (in black) and set the color of all pixels that have opacity 1 to black (i.e. all pixels that we haven't rasterized in the first step). This is necessary in case another object intersects with the mesh.
  • We render front faces again with the illumination from the front and add the color in the framebuffer multiplied with a factor that describes how close the point (on the front) is to a silhouette.

In the first and third step, we use the silhouette factor 1 - |N·L|, which is 1 at a silhouette and 0 if the viewer looks straight onto the surface. (An exponent for the dot product could be introduced to allow for more artistic control.) Thus, all the calculations are actually rather straightforward. The complicated part is the blending.

ImplementationEdit

The implementation relies heavily on blending, which is discussed in Section “Transparency”. In addition to three passes corresponding to the steps mentioned above, we also need two more additional passes for additional light sources on the back and the front. With so many passes, it makes sense to get a clear idea of what the render passes are supposed to do. To this end, a skeleton of the shader without the GLSL code is very helpful:

Shader "GLSL translucent bodies" {
   Properties {
      _Color ("Diffuse Color", Color) = (1,1,1,1) 
      _Waxiness ("Waxiness", Range(0,1)) = 0
      _SpecColor ("Specular Color", Color) = (1,1,1,1) 
      _Shininess ("Shininess", Float) = 10
      _TranslucentColor ("Translucent Color", Color) = (0,0,0,1)
   }
   SubShader {
      Pass {      
         Tags { "LightMode" = "ForwardBase" } // pass for 
            // ambient light and first light source on back faces
         Cull Front // render back faces only
         Blend One Zero // mark rasterized pixels in framebuffer 
            // with alpha = 0 (usually they should have alpha = 1)
 
         GLSLPROGRAM 
         [...] 
         ENDGLSL
      }
 
      Pass {      
         Tags { "LightMode" = "ForwardAdd" } 
            // pass for additional light sources on back faces
         Cull Front // render back faces only
         Blend One One // additive blending 
 
         GLSLPROGRAM 
         [...] 
         ENDGLSL
      }
 
      Pass {	
         Tags { "LightMode" = "ForwardBase" } // pass for 
            // setting pixels that were not rasterized to black
         Cull Back // render front faces only (default behavior)
         Blend Zero OneMinusDstAlpha // set colors of pixels 
            // with alpha = 1 to black by multiplying with 1-alpha
 
         GLSLPROGRAM                  
         #ifdef VERTEX         
         void main() { 
            gl_Position = gl_ModelViewProjectionMatrix * gl_Vertex; 
         }
         #endif
         #ifdef FRAGMENT         
         void main() { gl_FragColor = vec4(0.0); }         
         #endif
         ENDGLSL
      }
 
      Pass {      
         Tags { "LightMode" = "ForwardBase" } // pass for 
            // ambient light and first light source on front faces
         Cull Back // render front faces only
         Blend One SrcAlpha // multiply color in framebuffer 
            // with silhouetteness in fragment's alpha and add colors
 
         GLSLPROGRAM 
         [...] 
         ENDGLSL
      }
 
      Pass {      
         Tags { "LightMode" = "ForwardAdd" } 
            // pass for additional light sources on front faces
         Cull Back // render front faces only
         Blend One One // additive blending 
 
         GLSLPROGRAM 
         [...] 
         ENDGLSL
      }
   } 
   // The definition of a fallback shader should be commented out 
   // during development:
   // Fallback "Specular"
}

This skeleton is already quite long; however, it gives a good idea of how the overall shader is organized.

Complete Shader CodeEdit

In the following complete shader code, note that the property _TranslucentColor instead of _Color is used in the computation of the diffuse and ambient part on the back faces. Also note how the “silhouetteness” is computed on the back faces as well as on the front faces; however, it is directly multiplied only to the fragment color of the back faces. On the front faces, it is only indirectly multiplied through the alpha component of the fragment color and blending of this alpha with the destination color (the color of pixels in the framebuffer). Finally, the “waxiness” is only used for the diffuse reflection on the front faces.

Shader "GLSL translucent bodies" {
   Properties {
      _Color ("Diffuse Color", Color) = (1,1,1,1) 
      _Waxiness ("Waxiness", Range(0,1)) = 0
      _SpecColor ("Specular Color", Color) = (1,1,1,1) 
      _Shininess ("Shininess", Float) = 10
      _TranslucentColor ("Translucent Color", Color) = (0,0,0,1)
   }
   SubShader {
      Pass {      
         Tags { "LightMode" = "ForwardBase" } // pass for 
            // ambient light and first light source on back faces
         Cull Front // render back faces only
         Blend One Zero // mark rasterized pixels in framebuffer 
            // with alpha = 0 (usually they should have alpha = 1)
 
         GLSLPROGRAM
 
         // User-specified properties
         uniform vec4 _Color; 
         uniform float _Waxiness;
         uniform vec4 _SpecColor; 
         uniform float _Shininess;
         uniform vec4 _TranslucentColor; 
 
         // The following built-in uniforms (except _LightColor0) 
         // are also defined in "UnityCG.glslinc", 
         // i.e. one could #include "UnityCG.glslinc" 
         uniform vec3 _WorldSpaceCameraPos; 
            // camera position in world space
         uniform mat4 _Object2World; // model matrix
         uniform mat4 _World2Object; // inverse model matrix
         uniform vec4 _WorldSpaceLightPos0; 
            // direction to or position of light source
         uniform vec4 _LightColor0; 
            // color of light source (from "Lighting.cginc")
 
         varying vec4 position; 
            // position of the vertex (and fragment) in world space 
         varying vec3 varyingNormalDirection; 
            // surface normal vector in world space
 
         #ifdef VERTEX
 
         void main()
         {                                
            mat4 modelMatrix = _Object2World;
            mat4 modelMatrixInverse = _World2Object; // unity_Scale.w 
               // is unnecessary because we normalize vectors
 
            position = modelMatrix * gl_Vertex;
            varyingNormalDirection = normalize(vec3(
               vec4(gl_Normal, 0.0) * modelMatrixInverse));
 
            gl_Position = gl_ModelViewProjectionMatrix * gl_Vertex;
         }
 
         #endif
 
         #ifdef FRAGMENT
 
         void main()
         {
            vec3 normalDirection = normalize(varyingNormalDirection);
 
            vec3 viewDirection = 
               normalize(_WorldSpaceCameraPos - vec3(position));
            vec3 lightDirection;
            float attenuation;
 
            if (0.0 == _WorldSpaceLightPos0.w) // directional light?
            {
               attenuation = 1.0; // no attenuation
               lightDirection = normalize(vec3(_WorldSpaceLightPos0));
            } 
            else // point or spot light
            {
               vec3 vertexToLightSource = 
                  vec3(_WorldSpaceLightPos0 - position);
               float distance = length(vertexToLightSource);
               attenuation = 1.0 / distance; // linear attenuation 
               lightDirection = normalize(vertexToLightSource);
            }
 
            vec3 ambientLighting = 
               vec3(gl_LightModel.ambient) * vec3(_TranslucentColor);
 
            vec3 diffuseReflection = attenuation 
               * vec3(_LightColor0) * vec3(_TranslucentColor) 
               * max(0.0, dot(normalDirection, lightDirection));
 
            float silhouetteness = 
               1.0 - abs(dot(viewDirection, normalDirection));
 
            gl_FragColor = vec4(silhouetteness 
               * (ambientLighting + diffuseReflection), 0.0);
         }
 
         #endif
 
         ENDGLSL
      }
 
      Pass {      
         Tags { "LightMode" = "ForwardAdd" } 
            // pass for additional light sources on back faces
         Cull Front // render back faces only
         Blend One One // additive blending 
 
         GLSLPROGRAM
 
         // User-specified properties
         uniform vec4 _Color; 
         uniform float _Waxiness;
         uniform vec4 _SpecColor; 
         uniform float _Shininess;
         uniform vec4 _TranslucentColor; 
 
         // The following built-in uniforms (except _LightColor0) 
         // are also defined in "UnityCG.glslinc", 
         // i.e. one could #include "UnityCG.glslinc" 
         uniform vec3 _WorldSpaceCameraPos; 
            // camera position in world space
         uniform mat4 _Object2World; // model matrix
         uniform mat4 _World2Object; // inverse model matrix
         uniform vec4 _WorldSpaceLightPos0; 
            // direction to or position of light source
         uniform vec4 _LightColor0; 
            // color of light source (from "Lighting.cginc")
 
         varying vec4 position; 
            // position of the vertex (and fragment) in world space 
         varying vec3 varyingNormalDirection; 
            // surface normal vector in world space
 
         #ifdef VERTEX
 
         void main()
         {                                
            mat4 modelMatrix = _Object2World;
            mat4 modelMatrixInverse = _World2Object; // unity_Scale.w 
               // is unnecessary because we normalize vectors
 
            position = modelMatrix * gl_Vertex;
            varyingNormalDirection = normalize(vec3(
               vec4(gl_Normal, 0.0) * modelMatrixInverse));
 
            gl_Position = gl_ModelViewProjectionMatrix * gl_Vertex;
         }
 
         #endif
 
         #ifdef FRAGMENT
 
         void main()
         {
            vec3 normalDirection = normalize(varyingNormalDirection);
 
            vec3 viewDirection = 
               normalize(_WorldSpaceCameraPos - vec3(position));
            vec3 lightDirection;
            float attenuation;
 
            if (0.0 == _WorldSpaceLightPos0.w) // directional light?
            {
               attenuation = 1.0; // no attenuation
               lightDirection = normalize(vec3(_WorldSpaceLightPos0));
            } 
            else // point or spot light
            {
               vec3 vertexToLightSource = 
                  vec3(_WorldSpaceLightPos0 - position);
               float distance = length(vertexToLightSource);
               attenuation = 1.0 / distance; // linear attenuation 
               lightDirection = normalize(vertexToLightSource);
            }
 
            vec3 diffuseReflection = attenuation 
               * vec3(_LightColor0) * vec3(_TranslucentColor) 
               * max(0.0, dot(normalDirection, lightDirection));
 
            float silhouetteness = 
               1.0 - abs(dot(viewDirection, normalDirection));
 
            gl_FragColor = 
               vec4(silhouetteness * diffuseReflection, 0.0);
         }
 
         #endif
 
         ENDGLSL
      }
 
      Pass {	
         Tags { "LightMode" = "ForwardBase" } // pass for 
            // setting pixels that were not rasterized to black
         Cull Back // render front faces only (default behavior)
         Blend Zero OneMinusDstAlpha // set colors of pixels 
            // with alpha = 1 to black by multiplying with 1-alpha
 
         GLSLPROGRAM
 
         #ifdef VERTEX
 
         void main()
         {				
            gl_Position = gl_ModelViewProjectionMatrix * gl_Vertex;
         }
 
         #endif
 
         #ifdef FRAGMENT
 
         void main()
         {
            gl_FragColor = vec4(0.0);			
         }
 
         #endif
 
         ENDGLSL
      }
 
      Pass {      
         Tags { "LightMode" = "ForwardBase" } // pass for 
            // ambient light and first light source on front faces
         Cull Back // render front faces only
         Blend One SrcAlpha // multiply color in framebuffer 
            // with silhouetteness in fragment's alpha and add colors
 
         GLSLPROGRAM
 
         // User-specified properties
         uniform vec4 _Color; 
         uniform float _Waxiness;
         uniform vec4 _SpecColor; 
         uniform float _Shininess;
         uniform vec4 _TranslucentColor; 
 
         // The following built-in uniforms (except _LightColor0) 
         // are also defined in "UnityCG.glslinc", 
         // i.e. one could #include "UnityCG.glslinc" 
         uniform vec3 _WorldSpaceCameraPos; 
            // camera position in world space
         uniform mat4 _Object2World; // model matrix
         uniform mat4 _World2Object; // inverse model matrix
         uniform vec4 _WorldSpaceLightPos0; 
           // direction to or position of light source
         uniform vec4 _LightColor0; 
           // color of light source (from "Lighting.cginc")
 
         varying vec4 position; 
            // position of the vertex (and fragment) in world space 
         varying vec3 varyingNormalDirection; 
            // surface normal vector in world space
 
         #ifdef VERTEX
 
         void main()
         {                                
            mat4 modelMatrix = _Object2World;
            mat4 modelMatrixInverse = _World2Object; // unity_Scale.w 
               // is unnecessary because we normalize vectors
 
            position = modelMatrix * gl_Vertex;
            varyingNormalDirection = normalize(vec3(
               vec4(gl_Normal, 0.0) * modelMatrixInverse));
 
            gl_Position = gl_ModelViewProjectionMatrix * gl_Vertex;
         }
 
         #endif
 
         #ifdef FRAGMENT
 
         void main()
         {
            vec3 normalDirection = normalize(varyingNormalDirection);
 
            vec3 viewDirection = 
               normalize(_WorldSpaceCameraPos - vec3(position));
            vec3 lightDirection;
            float attenuation;
 
            if (0.0 == _WorldSpaceLightPos0.w) // directional light?
            {
               attenuation = 1.0; // no attenuation
               lightDirection = normalize(vec3(_WorldSpaceLightPos0));
            } 
            else // point or spot light
            {
               vec3 vertexToLightSource = 
                  vec3(_WorldSpaceLightPos0 - position);
               float distance = length(vertexToLightSource);
               attenuation = 1.0 / distance; // linear attenuation 
               lightDirection = normalize(vertexToLightSource);
            }
 
            vec3 ambientLighting = 
               vec3(gl_LightModel.ambient) * vec3(_Color);
 
            vec3 diffuseReflection = 
               attenuation * vec3(_LightColor0) * vec3(_Color) 
               * (_Waxiness + (1.0 - _Waxiness) 
               * max(0.0, dot(normalDirection, lightDirection)));
 
            vec3 specularReflection;
            if (dot(normalDirection, lightDirection) < 0.0) 
               // light source on the wrong side?
            {
               specularReflection = vec3(0.0, 0.0, 0.0); 
                  // no specular reflection
            }
            else // light source on the right side
            {
               specularReflection = attenuation * vec3(_LightColor0) 
                  * vec3(_SpecColor) * pow(max(0.0, dot(
                  reflect(-lightDirection, normalDirection), 
                  viewDirection)), _Shininess);
            }
 
            float silhouetteness = 
               1.0 - abs(dot(viewDirection, normalDirection));
 
            gl_FragColor = vec4(ambientLighting + diffuseReflection 
               + specularReflection, silhouetteness);
         }
 
         #endif
 
         ENDGLSL
      }
 
      Pass {      
         Tags { "LightMode" = "ForwardAdd" } 
            // pass for additional light sources on front faces
         Cull Back // render front faces only
         Blend One One // additive blending 
 
         GLSLPROGRAM
 
         // User-specified properties
         uniform vec4 _Color; 
         uniform float _Waxiness;
         uniform vec4 _SpecColor; 
         uniform float _Shininess;
         uniform vec4 _TranslucentColor; 
 
         // The following built-in uniforms (except _LightColor0) 
         // are also defined in "UnityCG.glslinc", 
         // i.e. one could #include "UnityCG.glslinc" 
         uniform vec3 _WorldSpaceCameraPos; 
            // camera position in world space
         uniform mat4 _Object2World; // model matrix
         uniform mat4 _World2Object; // inverse model matrix
         uniform vec4 _WorldSpaceLightPos0; 
            // direction to or position of light source
         uniform vec4 _LightColor0; 
            // color of light source (from "Lighting.cginc")
 
         varying vec4 position; 
            // position of the vertex (and fragment) in world space 
         varying vec3 varyingNormalDirection; 
            // surface normal vector in world space
 
         #ifdef VERTEX
 
         void main()
         {                                
            mat4 modelMatrix = _Object2World;
            mat4 modelMatrixInverse = _World2Object; // unity_Scale.w 
               // is unnecessary because we normalize vectors
 
            position = modelMatrix * gl_Vertex;
            varyingNormalDirection = normalize(vec3(
               vec4(gl_Normal, 0.0) * modelMatrixInverse));
 
            gl_Position = gl_ModelViewProjectionMatrix * gl_Vertex;
         }
 
         #endif
 
         #ifdef FRAGMENT
 
         void main()
         {
            vec3 normalDirection = normalize(varyingNormalDirection);
 
            vec3 viewDirection = 
               normalize(_WorldSpaceCameraPos - vec3(position));
            vec3 lightDirection;
            float attenuation;
 
            if (0.0 == _WorldSpaceLightPos0.w) // directional light?
            {
               attenuation = 1.0; // no attenuation
               lightDirection = normalize(vec3(_WorldSpaceLightPos0));
            } 
            else // point or spot light
            {
               vec3 vertexToLightSource = 
                  vec3(_WorldSpaceLightPos0 - position);
               float distance = length(vertexToLightSource);
               attenuation = 1.0 / distance; // linear attenuation 
               lightDirection = normalize(vertexToLightSource);
            }
 
            vec3 diffuseReflection = 
               attenuation * vec3(_LightColor0) * vec3(_Color) 
               * (_Waxiness + (1.0 - _Waxiness) 
               * max(0.0, dot(normalDirection, lightDirection)));
 
            vec3 specularReflection;
            if (dot(normalDirection, lightDirection) < 0.0) 
               // light source on the wrong side?
            {
               specularReflection = vec3(0.0, 0.0, 0.0); 
                  // no specular reflection
            }
            else // light source on the right side
            {
               specularReflection = attenuation * vec3(_LightColor0) 
                  * vec3(_SpecColor) * pow(max(0.0, dot(
                  reflect(-lightDirection, normalDirection), 
                  viewDirection)), _Shininess);
            }
 
            gl_FragColor = 
               vec4(diffuseReflection + specularReflection, 1.0);
         }
 
         #endif
 
         ENDGLSL
      }
   } 
   // The definition of a fallback shader should be commented out 
   // during development:
   // Fallback "Specular"
}

SummaryEdit

Congratulations! You finished this tutorial on translucent bodies, which was mainly about:

  • How to fake the appearance of wax.
  • How to fake the appearance of silhouettes of translucent materials lit by backlight.
  • How to implement these techniques.

Further ReadingEdit

If you still want to know more


< GLSL Programming/Unity

Unless stated otherwise, all example source code on this page is granted to the public domain.
Last modified on 16 August 2012, at 15:45