# GLSL Programming/Unity/Translucent Bodies

## Contents

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.

### Waxiness

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 ${\displaystyle w}$ increases from 0 to 1. More specifically, we multiply the term max(0, N·L) with ${\displaystyle 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 ${\displaystyle 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:

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

we get:

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

with the waxiness ${\displaystyle 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 Backlight

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.

### Implementation

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)
}
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.

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)
}
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"
}


### Summary

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.