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.

### 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 increases from 0 to 1. More specifically, we multiply the term max(0, **N**·**L**) with . However, this will not only reduce the contrast but also the overall brightness of the illumination. To avoid this, we add the waxiness 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:

we get:

with the waxiness 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.

### 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

- about translucent surfaces, you should read Section “Translucent Surfaces”.
- about the diffuse term of the Phong reflection model, you should read Section “Diffuse Reflection”.
- about the ambient or the specular term of the Phong reflection model, you should read Section “Specular Highlights”.
- about per-pixel lighting with the Phong reflection model, you should read Section “Smooth Specular Highlights”.
- about basic real-time techniques for subsurface scattering, you could read Chapter 16, “Real-Time Approximations to Subsurface Scattering” by Simon Green of the book “GPU Gems” by Randima Fernando (editor) published 2004 by Addison-Wesley, which is available online.