5. Lighting Model Sample Implementation

Lighting models are mathematical models that express how a three-dimensional scene is lit. They include information such as the colors of the light sources and materials, and how the light is reflected.

A typical lighting model can be broken down into several components: emissive, ambient, diffuse, and specular light.

Fragment lighting outputs a primary and a secondary color. The primary color is the sum of the emissive, ambient, and diffuse light, while the secondary color is the sum of two specular lights and can be used to express more than just simple specular reflection.

5.1. Microfacet Reflection

Microfacets are tiny surfaces that cannot be seen with the naked eye. Assuming that a rough surface is composed of many microfacets, those microfacets whose normals coincide with the half-angle vector of the light vector and the view vector reflect light along the line of sight toward the viewer. In other words, the greater the number of microfacets there are that reflect along the line of sight, the stronger the reflected light that the viewer sees.

If you implement a distribution function that returns the percentage of microfacets whose normals coincide with the angle supplied as an argument, the amount of light reflected by a substance will be proportional to that function. This function also shows how the microfacet slopes are distributed, so it can also be called a slope distribution function.

Code 5-1. Microfacet Reflection
GLfloat ms0[] = {1.0f, 1.0f, 1.0f, 1.0f};
GLfloat ms1[] = {0.0f, 0.0f, 0.0f, 1.0f};
glUniform1i(glGetUniformLocation(progID, "dmp_LightEnv.config"), 
        GL_LIGHT_ENV_LAYER_CONFIG0_DMP);
glUniform4fv(glGetUniformLocation(progID, "dmp_FragmentMaterial.specular0"), 
        1, ms0);
glUniform4fv(glGetUniformLocation(progID, "dmp_FragmentMaterial.specular1"), 
        1, ms1);
glUniform1i(glGetUniformLocation(progID, "dmp_LightEnv.lutEnabledRefl"), 
        GL_FALSE);
glUniform1i(glGetUniformLocation(progID, "dmp_LightEnv.lutEnabledD0"), 
        GL_TRUE);
glUniform1i(glGetUniformLocation(progID, "dmp_LightEnv.absLutInputD0"), 
        GL_TRUE);
glUniform1i(glGetUniformLocation(progID, "dmp_LightEnv.lutInputD0"), 
        GL_LIGHT_ENV_NH_DMP);
glUniform1i(glGetUniformLocation(progID, "dmp_LightEnv.clampHighlights"), 
        GL_FALSE);
glUniform1i(glGetUniformLocation(progID, "dmp_LightSource[0].geomFactor1"), 
        GL_FALSE);

GLfloat LUT_D0[512];
for( i = 0; i < 256; i++) LUT_D0[i] = slopeDist((float) i / 256.0f);
for( i = 0; i < 255; i++) LUT_D0[i + 256] = LUT_D0[i + 1] - LUT_D0[i];
LUT_D0[511] = slopeDist(1.0f) - LUT_D0[255];

glBindTexture(GL_LUT_TEXTURE0_DMP, lutTexD0);
glTexImage1D(GL_LUT_TEXTURE0_DMP, 0, GL_LUMINANCEF_DMP, 512, 0, 
        GL_LUMINANCEF_DMP, GL_FLOAT, LUT_D0);
glUniform1i(glGetUniformLocation(progID, 
        "dmp_FragmentMaterial.samplerD0"), 0);

Set the slope distribution sampling values in the distribution factor 0 (D0) lookup table.

5.2. Blinn-Phong Model

The Blinn-Phong reflection model is a simple microfacet reflection model that uses an exponential function as its distribution function. The generalized Blinn-Phong model, in addition to the exponential function, also incorporates another distribution function that takes the dot product of the normal and the half-angle vector as an argument. The Gaussian function is one example of a possible distribution function it can use.

A simple Blinn-Phong reflection model can implement microfacet reflection using only the slopeDist distribution function, as defined below.

Code 5-2. Distribution Function in a Simple Blinn-Phong Model
float slopeDist(float NH)
{
    return powf(NH, 2.0f);
}

It is also possible to represent a two-layered Blinn-Phong model using the two specular lights in the secondary color. Slope distribution sampling values for the second layer are set in the distribution factor 1 (D1) lookup table, and this can be used by changing to a layer configuration that uses both D0 and D1.

Code 5-3. Two-Layer Blinn-Phong Model
GLfloat ms0[] = {1.0f, 1.0f, 1.0f, 1.0f};
GLfloat ms1[] = {1.0f, 1.0f, 1.0f, 1.0f};
glUniform1i(glGetUniformLocation(progID, "dmp_LightEnv.config"), 
        GL_LIGHT_ENV_LAYER_CONFIG2_DMP);
glUniform4fv(glGetUniformLocation(progID, "dmp_FragmentMaterial.specular0"), 
        1, ms0);
glUniform4fv(glGetUniformLocation(progID, "dmp_FragmentMaterial.specular1"), 
        1, ms1);
glUniform1i(glGetUniformLocation(progID, "dmp_LightEnv.lutEnabledRefl"), 
        GL_FALSE);
glUniform1i(glGetUniformLocation(progID, "dmp_LightEnv.lutEnabledD0"), 
        GL_TRUE);
glUniform1i(glGetUniformLocation(progID, "dmp_LightEnv.absLutInputD0"), 
        GL_TRUE);
glUniform1i(glGetUniformLocation(progID, "dmp_LightEnv.lutInputD0"), 
        GL_LIGHT_ENV_NH_DMP);
glUniform1i(glGetUniformLocation(progID, "dmp_LightEnv.lutEnabledD1"), 
        GL_TRUE);
glUniform1i(glGetUniformLocation(progID, "dmp_LightEnv.absLutInputD1"), 
        GL_TRUE);
glUniform1i(glGetUniformLocation(progID, "dmp_LightEnv.lutInputD1"), 
        GL_LIGHT_ENV_NH_DMP);

glUniform1i(glGetUniformLocation(progID, "dmp_LightEnv.clampHighlights"), 
        GL_FALSE);
glUniform1i(glGetUniformLocation(progID, "dmp_LightSource[0].geomFactor0"), 
        GL_FALSE);
glUniform1i(glGetUniformLocation(progID, "dmp_LightSource[0].geomFactor1"), 
        GL_FALSE);

GLfloat LUT_D0[512], LUT_D1[512];
for( i = 0; i < 256; i++) LUT_D0[i] = slopeDistD0((float) i / 256.0f);
for( i = 0; i < 255; i++) LUT_D0[i + 256] = LUT_D0[i + 1] - LUT_D0[i];
LUT_D0[511] = slopeDistD0(1.0f) - LUT_D0[255];

for( i = 0; i < 256; i++) LUT_D1[i] = slopeDistD1((float) i / 256.0f);
for( i = 0; i < 255; i++) LUT_D1[i + 256] = LUT_D1[i + 1] - LUT_D1[i];
LUT_D1[511] = slopeDistD1(1.0f) - LUT_D1[255];

glBindTexture(GL_LUT_TEXTURE0_DMP, lutTexD0);
glTexImage1D(GL_LUT_TEXTURE0_DMP, 0, GL_LUMINANCEF_DMP, 512, 0, 
        GL_LUMINANCEF_DMP, GL_FLOAT, LUT_D0);
glBindTexture(GL_LUT_TEXTURE1_DMP, lutTexD1);
glTexImage1D(GL_LUT_TEXTURE1_DMP, 0, GL_LUMINANCEF_DMP, 512, 0, 
        GL_LUMINANCEF_DMP, GL_FLOAT, LUT_D1);
glUniform1i(glGetUniformLocation(progID, "dmp_FragmentMaterial.samplerD0"), 0);
glUniform1i(glGetUniformLocation(progID, "dmp_FragmentMaterial.samplerD1"), 1);

For more information about the Blinn-Phong model, see the following.

Blinn, James F., Models of Light Reflection for Computer Synthesized Pictures, ACM Computer Graphics, SIGGRAPH 1977 Proceedings, 11(4), pp. 192-198

5.3. Cook-Torrance Model

The Cook-Torrance model more accurately represents the amount of reflected light. This model defines the amount of reflected light as proportional to a distribution function multiplied by a Fresnel factor. By representing the refraction and reflection of transmitted light as a function of the angle of incidence and the refractive index, and by considering the different absorption coefficients of different substances, it is possible for this to model the reflections of various different substances. The Fresnel factor expresses the reflection as a function of the light vector and the normal vector.

When considering microfacet Fresnel factors, the normals in question are the normals of the microfacets. Just as in microfacet reflection, these normals coincide with the half-angle vector. Consequently, when this model is applied to fragment lighting, the view vector and half-angle vector (which bisects the angle formed by the view and light vectors) are used as inputs for the Fresnel factor, and the distribution function is a Beckmann function that excludes the m2 denominator. The shadows cast by the microfacets onto other microfacets must also be considered, but the proportion of the microfacet reflection that reaches the viewpoint (which depends on the microfacet angle) is already included in the secondary color calculation in the form of geometry factors. These geometry factors are similar to the Cook-Torrance geometric factors, but also take angle into account. The 3DS geometry factors approximate the Cook-Torrance geometric factors as follows.

Lfi is the i light vector, and Nf and Vf are the normal and view vectors.

The refractive index and absorption coefficient of the Fresnel factor depend on the color. The Fresnel factor can thus be calculated using the refractive indexes and absorption coefficients of the individual RGB components, and handled as the reflection per each component in fragment lighting.

Given this, reflections, distribution factors, and geometry factors are used to represent the Cook-Torrance model in fragment lighting. The dot product of the view vector and the half-angle vector is used as the input for reflection, and the dot product of the normal and the half-angle vector is used as the input for the distribution factors. This is possible for layer configurations 4, 5, and 7. Note that specular light 1 of the material is replaced with the reflection, and that the color is determined by specular light 1 from the light.

Code 5-4. Cook-Torrance Model
GLfloat ms[] = {0.0f, 0.0f, 0.0f, 0.0f};
GLfloat ls[] = {1.0f, 1.0f, 1.0f, 1.0f};

glUniform4fv(glGetUniformLocation(progID, "dmp_FragmentMaterial.specular0"), 
        1, ms);
glUniform4fv(glGetUniformLocation(progID, 
        "dmp_FragmentLightSource[0].specular1"), 1, ls);
glUniform1i(glGetUniformLocation(progID, "dmp_LightEnv.lutEnabledRefl"), 
        GL_TRUE);
glUniform1i(glGetUniformLocation(progID, "dmp_LightEnv.config"), 
        GL_LIGHT_ENV_LAYER_CONFIG4_DMP);

glUniform1i(glGetUniformLocation(progID, "dmp_LightEnv.lutEnabledD1"), 
        GL_TRUE);
glUniform1i(glGetUniformLocation(progID, "dmp_LightEnv.absLutInputRR"), 
        GL_FALSE);
glUniform1i(glGetUniformLocation(progID, "dmp_LightEnv.absLutInputRG"), 
        GL_FALSE);
glUniform1i(glGetUniformLocation(progID, "dmp_LightEnv.absLutInputRB"), 
        GL_FALSE);
glUniform1i(glGetUniformLocation(progID, "dmp_LightEnv.absLutInputD1"), 
        GL_FALSE);
glUniform1i(glGetUniformLocation(progID, "dmp_LightEnv.lutInputRR"), 
        GL_LIGHT_ENV_VH_DMP);
glUniform1i(glGetUniformLocation(progID, "dmp_LightEnv.lutInputRG"), 
        GL_LIGHT_ENV_VH_DMP);
glUniform1i(glGetUniformLocation(progID, "dmp_LightEnv.lutInputRB"), 
        GL_LIGHT_ENV_VH_DMP);
glUniform1i(glGetUniformLocation(progID, "dmp_LightEnv.lutInputD1"), 
        GL_LIGHT_ENV_NH_DMP);
glUniform1f(glGetUniformLocation(progID, "dmp_LightEnv.lutScaleRR"), 2.0f);
glUniform1f(glGetUniformLocation(progID, "dmp_LightEnv.lutScaleRG"), 2.0f);
glUniform1f(glGetUniformLocation(progID, "dmp_LightEnv.lutScaleRB"), 2.0f);
glUniform1f(glGetUniformLocation(progID, "dmp_LightEnv.lutScaleD1"), 2.0f);
glUniform1i(glGetUniformLocation(progID, "dmp_LightEnv.clampHighlights"), 
        GL_FALSE);
glUniform1i(glGetUniformLocation(progID, "dmp_LightSource[0].geomFactor1"), 
        GL_TRUE);

GLfloat LUT_RR[512], LUT_RG[512], LUT_RB[512], LUT_D1[512];
for( i = 0; i < 128; i++) LUT_RR[i] = 
        nk_fresnel((float) i / 128.0f, FRESNEL_N_R, FRESNEL_K_R);
for( i = 0; i < 127; i++) LUT_RR[i + 256] = LUT_RR[i + 1] - LUT_RR[i];
LUT_RR[383] = nk_fresnel(1.0f, FRESNEL_N_R, FRESNEL_K_R) - LUT_RR[127];

for( i = 0; i < 128; i++) LUT_RG[i] = 
        nk_fresnel((float) i / 128.0f, FRESNEL_N_G, FRESNEL_K_G);
for( i = 0; i < 127; i++) LUT_RG[i + 256] = LUT_RG[i + 1] - LUT_RG[i];
LUT_RG[383] = nk_fresnel(1.0f, FRESNEL_N_G, FRESNEL_K_G) - LUT_RG[127];

for( i = 0; i < 128; i++) LUT_RB[i] = 
        nk_fresnel((float) i / 128.0f, FRESNEL_N_B, FRESNEL_K_B);
for( i = 0; i < 127; i++) LUT_RB[i + 256] = LUT_RB[i + 1] - LUT_RB[i];
LUT_RB[383] = nk_fresnel(1.0f, FRESNEL_N_B, FRESNEL_K_B) - LUT_RB[127];

for( i = 0; i < 128; i++) LUT_D1[i] = beckmann((float) i / 128.0f, 1.0f);
for( i = 0; i < 127; i++) LUT_D1[i + 256] = LUT_D1[i + 1] - LUT_D1[i];
LUT_D1[383] = beckmann(1.0f, 1.0f) - LUT_D1[127];

glBindTexture(GL_LUT_TEXTURE0_DMP, lutTexRR);
glTexImage1D(GL_LUT_TEXTURE0_DMP, 0, GL_LUMINANCEF_DMP, 512, 0, 
        GL_LUMINANCEF_DMP, GL_FLOAT, LUT_RR);
glBindTexture(GL_LUT_TEXTURE1_DMP, lutTexRG);
glTexImage1D(GL_LUT_TEXTURE1_DMP, 0, GL_LUMINANCEF_DMP, 512, 0, 
        GL_LUMINANCEF_DMP, GL_FLOAT, LUT_RG);
glBindTexture(GL_LUT_TEXTURE2_DMP, lutTexRB);
glTexImage1D(GL_LUT_TEXTURE2_DMP, 0, GL_LUMINANCEF_DMP, 512, 0, 
        GL_LUMINANCEF_DMP, GL_FLOAT, LUT_RB);
glBindTexture(GL_LUT_TEXTURE3_DMP, lutTexD1);
glTexImage1D(GL_LUT_TEXTURE3_DMP, 0, GL_LUMINANCEF_DMP, 512, 0, 
        GL_LUMINANCEF_DMP, GL_FLOAT, LUT_D1);
glUniform1i(glGetUniformLocation(progID, "dmp_FragmentMaterial.samplerRR"), 0);
glUniform1i(glGetUniformLocation(progID, "dmp_FragmentMaterial.samplerRG"), 1);
glUniform1i(glGetUniformLocation(progID, "dmp_FragmentMaterial.samplerRB"), 2);
glUniform1i(glGetUniformLocation(progID, "dmp_FragmentMaterial.samplerD1"), 3);

The nk_fresnel() function is a Fresnel reflection function that takes the light absorption coefficient into account, and beckmann is, unsurprisingly, a Beckmann function.

The dmp_LightEnv.lutScaleXX uniforms are used to double the lookup table outputs.

For more information about the Cook-Torrance model, see the following.

Cook, Robert L., and Torrance, Kenneth E., A Reflectance Model for Computer Graphics, ACM Computer Graphics, SIGGRAPH 1981 Proceedings, 15(4), pp. 307-316.

For more information about geometry factors, see the following.

Alan Watt, 3D Computer Graphics, 3rd edition, Addison-Wesley Publishing Ltd, Addison-Wesley Publishing Company Inc., 2000, pp. 216

For more information about the refraction indices and absorption coefficients of various substances, see the following.

Glassner, Andrew S., Principles of Digital Image Synthesis, Morgan Kaufmann Publishers, Inc., San Francisco, CA, 1995.

5.4. Schlick Anisotropic Model

The lighting models introduced previously assume that all facing surfaces have the same qualities (that is, they are isotropic), such that reflections are always symmetric with respect to the normal. Conversely, a substance is described as anisotropic if its surfaces have different properties depending on their orientation, such as the way a fabric surface like velvet might reflect differently for different orientations.

The light-scattering properties of an anisotropic surface vary as a function of the direction along the surface over which you measure those properties. Schlick proposed using a distribution function made up of two functions to represent anisotropic reflection. One function depends on the normal and the half-angle vector. The other depends on the angle Φ formed by the tangent and the vector projected by the half-angle vector on the tangent plane, as represented by the following equations.

r is a parameter characterizing the roughness of the surface, and p is a parameter characterizing the anisotropy of the surface.

The geometric factor in this model differs from that used in the Cook-Torrance model, but it has properties similar to the geometry factors used in fragment lighting.

Code 5-5. Schlick Anisotropic Model
glUniform1i(glGetUniformLocation(progID, "dmp_LightEnv.lutEnabledRefl"), 
        GL_TRUE);
glUniform1i(glGetUniformLocation(progID, "dmp_LightEnv.config"), 
        GL_LIGHT_ENV_LAYER_CONFIG7_DMP);

glUniform1i(glGetUniformLocation(progID, "dmp_LightEnv.lutEnabledD1"), 
        GL_TRUE);
glUniform1i(glGetUniformLocation(progID, "dmp_LightEnv.absLutInputRR"), 
        GL_TRUE);
glUniform1i(glGetUniformLocation(progID, "dmp_LightEnv.absLutInputRG"), 
        GL_TRUE);
glUniform1i(glGetUniformLocation(progID, "dmp_LightEnv.absLutInputRB"), 
        GL_TRUE);
glUniform1i(glGetUniformLocation(progID, "dmp_LightEnv.absLutInputD1"), 
        GL_TRUE);
glUniform1i(glGetUniformLocation(progID, "dmp_LightEnv.lutInputRR"), 
        GL_LIGHT_ENV_NH_DMP);
glUniform1i(glGetUniformLocation(progID, "dmp_LightEnv.lutInputRG"), 
        GL_LIGHT_ENV_NH_DMP);
glUniform1i(glGetUniformLocation(progID, "dmp_LightEnv.lutInputRB"), 
        GL_LIGHT_ENV_NH_DMP);
glUniform1i(glGetUniformLocation(progID, "dmp_LightEnv.lutInputD1"), 
        GL_LIGHT_ENV_CP_DMP);
glUniform1f(glGetUniformLocation(progID, "dmp_LightEnv.lutScaleRR"), 1.0f);
glUniform1f(glGetUniformLocation(progID, "dmp_LightEnv.lutScaleRG"), 1.0f);
glUniform1f(glGetUniformLocation(progID, "dmp_LightEnv.lutScaleRB"), 1.0f);
glUniform1f(glGetUniformLocation(progID, "dmp_LightEnv.lutScaleD1"), 1.0f);
glUniform1i(glGetUniformLocation(progID, "dmp_LightEnv.clampHighlights"), 
        GL_FALSE);
glUniform1i(glGetUniformLocation(progID, "dmp_LightSource[0].geomFactor0"), 
        GL_FALSE);
glUniform1i(glGetUniformLocation(progID, "dmp_LightSource[0].geomFactor1"), 
        GL_FALSE);

GLfloat LUT_RR[512], LUT_RG[512], LUT_RB[512], LUT_D1[512];
for( i = 0; i < 256; i++) LUT_RR[i] = 
        z_schlick(0.7f, (float) i / 256.0f, true);
for( i = 0; i < 255; i++) LUT_RR[i + 256] = LUT_RR[i + 1] - LUT_RR[i];
LUT_RR[511] = z_schlick(0.7f, 1.0f, true) - LUT_RR[255];

for( i = 0; i < 256; i++) LUT_RG[i] = 
        z_schlick(0.7f, (float) i / 256.0f, true);
for( i = 0; i < 255; i++) LUT_RG[i + 256] = LUT_RG[i + 1] - LUT_RG[i];
LUT_RG[511] = z_schlick(0.7f, 1.0f, true) - LUT_RG[255];

for( i = 0; i < 256; i++) LUT_RB[i] = 
        z_schlick(0.7f, (float) i / 256.0f, true);
for( i = 0; i < 255; i++) LUT_RB[i + 256] = LUT_RB[i + 1] - LUT_RB[i];
LUT_RB[511] = z_schlick(0.5f, 1.0f, true) - LUT_RB[255];

for( i = 0; i < 256; i++) LUT_D1[i] = 
        a_schlick(0.0015f, (float) i / 256.0f, true);
for( i = 0; i < 255; i++) LUT_D1[i + 256] = LUT_D1[i + 1] - LUT_D1[i];
LUT_D1[511] = a_schlick(0.0015f, 1.0f, true) - LUT_D1[255];

glBindTexture(GL_LUT_TEXTURE0_DMP, lutTexRR);
glTexImage1D(GL_LUT_TEXTURE0_DMP, 0, GL_LUMINANCEF_DMP, 512, 0, 
        GL_LUMINANCEF_DMP, GL_FLOAT, LUT_RR);
glBindTexture(GL_LUT_TEXTURE1_DMP, lutTexRG);
glTexImage1D(GL_LUT_TEXTURE1_DMP, 0, GL_LUMINANCEF_DMP, 512, 0, 
        GL_LUMINANCEF_DMP, GL_FLOAT, LUT_RG);
glBindTexture(GL_LUT_TEXTURE2_DMP, lutTexRB);
glTexImage1D(GL_LUT_TEXTURE2_DMP, 0, GL_LUMINANCEF_DMP, 512, 0, 
        GL_LUMINANCEF_DMP, GL_FLOAT, LUT_RB);
glBindTexture(GL_LUT_TEXTURE3_DMP, lutTexD1);
glTexImage1D(GL_LUT_TEXTURE3_DMP, 0, GL_LUMINANCEF_DMP, 512, 0, 
        GL_LUMINANCEF_DMP, GL_FLOAT, LUT_D1);
glUniform1i(glGetUniformLocation(progID, 
        "dmp_FragmentMaterial.samplerRR"), 0);
glUniform1i(glGetUniformLocation(progID, 
        "dmp_FragmentMaterial.samplerRG"), 1);
glUniform1i(glGetUniformLocation(progID, 
        "dmp_FragmentMaterial.samplerRB"), 2);
glUniform1i(glGetUniformLocation(progID, 
        "dmp_FragmentMaterial.samplerD1"), 3);
glUniform1i(glGetUniformLocation(progID, "dmp_LightEnv.bumpMode"), 
        GL_LIGHT_ENV_BUMP_AS_BUMP_DMP);
glUniform1i(glGetUniformLocation(progID, "dmp_LightEnv.bumpSelector"), 
        GL_TEXTURE0);

The z_schlick and a_schlick() functions are both Schlick functions.

This sample includes bump mapping because it needs perturbed tangents for input.

For more information about the Schlick anisotropic model, see the following reference.

Schlick, Christophe. An Inexpensive BRDF Model for Physically Based Rendering, Computer Graphics Forum, 13(3), pp. 233-246 (1994).

5.5. Subsurface-Scattering Model

When light enters an object made of a translucent material like skin, wax, or marble, the light is scattered internally in many directions before exiting the object. In such objects, light does not reflect from the same position at which it hits the object, so reflection cannot be represented using any of the models introduced so far.

This model, based on the light scattering theory proposed by Jensen et al., takes the integral of the incident light and the diffuse reflection function over a certain region of the surface. The region over which to integrate is of roughly the same size as the mean free path. For typical materials, this is between one and several millimeters.

When objects that are translucent, such as a human hand, are illuminated by a single light source against a dark background, the convex portions appear the most translucent. This is also true when using multiple light sources, so we can say that the most important part of rendering objects that have subsurface scattering is rendering their convex surfaces well.

With that in mind, if we consider the convex surfaces to be partial surfaces of spheres with radius S, we can take the integral of those surfaces using the formula proposed by Jensen et al. (radius S is much longer than the length of the "mean free path"). If we additionally consider the case of a rough surface, the reflection model can be expressed using the equation below.

is the sum of the Lambertian (diffuse light) term and the wrapping term. (The wrapping term represents penetration of light to surface areas where light is not directly incident.)



See the references below for α' (albedo) and Fdr.

Jensen, H. W., Marschner, S., Levoy, M., and Hanrahan, P., A practical model for subsurface light transport, SIGGRAPH 2001 Proceedings, E. Fiume, Ed., Annual Conference Series, pp. 511–518.

Code 5-6. Subsurface Scattering Model
 /* Lighting Environment */
glUniform1i(glGetUniformLocation(progID, "dmp_LightEnv.config" ), 
    GL_LIGHT_ENV_LAYER_CONFIG7_DMP);
glUniform1i(glGetUniformLocation(progID, "dmp_LightEnv.lutEnabledRefl" ), 
    GL_TRUE);
glUniform1i(glGetUniformLocation(progID, "dmp_LightEnv.fresnelSelector" ), 
    GL_LIGHT_ENV_PRI_SEC_ALPHA_FRESNEL_DMP);
glUniform1i(glGetUniformLocation(progID, "dmp_LightEnv.clampHighlights" ), 
    GL_FALSE);
glUniform1i(glGetUniformLocation(progID, "dmp_LightEnv.lutEnabledD0" ), 
    GL_TRUE);
glUniform1i(glGetUniformLocation(progID, "dmp_LightEnv.lutEnabledD1" ), 
    GL_TRUE);
glUniform1i(glGetUniformLocation(progID, "dmp_LightEnv.absLutInputRR" ), 
    GL_FALSE);
glUniform1i(glGetUniformLocation(progID, "dmp_LightEnv.absLutInputRG" ), 
    GL_FALSE);
glUniform1i(glGetUniformLocation(progID, "dmp_LightEnv.absLutInputRB" ), 
    GL_FALSE);
/* below we use GL_TRUE to have nonzero for negative NV because negative NV values happen on silhouette */
glUniform1i(glGetUniformLocation(progID, "dmp_LightEnv.absLutInputD1" ), 
    GL_TRUE);
glUniform1i(glGetUniformLocation(progID, "dmp_LightEnv.absLutInputD0" ), 
    GL_FALSE);
/* below we use GL_TRUE to have nonzero for negative NV because negative NV values happen on silhouette */
glUniform1i(glGetUniformLocation(progID, "dmp_LightEnv.absLutInputFR" ), 
    GL_TRUE);
glUniform1i(glGetUniformLocation(progID, "dmp_LightEnv.lutInputRR" ), 
    GL_LIGHT_ENV_LN_DMP);
glUniform1i(glGetUniformLocation(progID, "dmp_LightEnv.lutInputRG" ), 
    GL_LIGHT_ENV_LN_DMP);
glUniform1i(glGetUniformLocation(progID, "dmp_LightEnv.lutInputRB" ), 
    GL_LIGHT_ENV_LN_DMP);
glUniform1i(glGetUniformLocation(progID, "dmp_LightEnv.lutInputD1" ), 
    GL_LIGHT_ENV_NV_DMP);
glUniform1i(glGetUniformLocation(progID, "dmp_LightEnv.lutInputD0" ), 
    GL_LIGHT_ENV_NH_DMP);
glUniform1i(glGetUniformLocation(progID, "dmp_LightEnv.lutInputFR" ), 
    GL_LIGHT_ENV_NV_DMP);

/* Material */
GLfloat ms2[] = {0.28f, 0.28f, 0.28f, 1.f};
glUniform4fv(glGetUniformLocation(progID, "dmp_FragmentMaterial.specular1"), 1, ms2);
glUniform1i(glGetUniformLocation(progID, "dmp_FragmentMaterial.samplerRR"), 0);
glUniform1i(glGetUniformLocation(progID, "dmp_FragmentMaterial.samplerRG"), 1);
glUniform1i(glGetUniformLocation(progID, "dmp_FragmentMaterial.samplerRB"), 2);
glUniform1i(glGetUniformLocation(progID, "dmp_FragmentMaterial.samplerD1"), 3);
glUniform1i(glGetUniformLocation(progID, "dmp_FragmentMaterial.samplerD0"), 4);
glUniform1i(glGetUniformLocation(progID, "dmp_FragmentMaterial.samplerFR"), 5);

/* Light Source */
GLfloat ld0[] = {1.f, 1.f, 1.f, 1.f}; /* light0 diffuse */
GLfloat ls0[] = {0.35f, 0.35f, 0.35f, 1.f}; /* light0 specular */
GLfloat ls1[] = {0.28f, 0.28f, 0.28f, 1.f}; /* light0 specular2 */
glUniform4fv(glGetUniformLocation(progID, "dmp_FragmentLightSource[0].diffuse"), 
    1, ld0);
glUniform4fv(glGetUniformLocation(progID, 
    "dmp_FragmentLightSource[0].specular0"), 1, ls0);
glUniform4fv(glGetUniformLocation(progID, 
    "dmp_FragmentLightSource[0].specular1"), 1, ls1);
glUniform1i(glGetUniformLocation(progID, 
    "dmp_FragmentLightSource[0].geomFactor0" ), GL_FALSE);
glUniform1i(glGetUniformLocation(progID, 
    "dmp_FragmentLightSource[0].geomFactor1" ), GL_FALSE);

/* Texture Combiner 0 */
glUniform1i(glGetUniformLocation(progID, "dmp_TexEnv[0].combineRgb"), 
    GL_ADD);
glUniform1i(glGetUniformLocation(progID, "dmp_TexEnv[0].combineAlpha"), 
    GL_REPLACE);
glUniform3i(glGetUniformLocation(progID, "dmp_TexEnv[0].operandRgb"), 
    GL_SRC_COLOR, GL_SRC_COLOR, GL_SRC_COLOR);
glUniform3i(glGetUniformLocation(progID, "dmp_TexEnv[0].operandAlpha"), 
    GL_SRC_ALPHA, GL_SRC_ALPHA, GL_SRC_ALPHA);
glUniform3i(glGetUniformLocation(progID, "dmp_TexEnv[0].srcRgb"), 
    GL_FRAGMENT_PRIMARY_COLOR_DMP, GL_FRAGMENT_SECONDARY_COLOR_DMP, 
    GL_CONSTANT);
glUniform3i(glGetUniformLocation(progID, "dmp_TexEnv[0].srcAlpha"), 
    GL_CONSTANT, GL_CONSTANT, GL_CONSTANT);

/* Texture Combiner 1 */
glUniform1i(glGetUniformLocation(progID, "dmp_Texture[0].samplerType" ), 
    GL_TEXTURE_CUBE_MAP);
glUniform1i(glGetUniformLocation(progID, "dmp_TexEnv[1].combineRgb"), 
    GL_MULT_ADD_DMP);
glUniform1i(glGetUniformLocation(progID, "dmp_TexEnv[1].combineAlpha"), 
    GL_REPLACE);
glUniform3i(glGetUniformLocation(progID, "dmp_TexEnv[1].operandRgb"), 
    GL_SRC_COLOR, GL_SRC_ALPHA, GL_SRC_COLOR);
glUniform3i(glGetUniformLocation(progID, "dmp_TexEnv[1].operandAlpha"), 
    GL_SRC_ALPHA, GL_SRC_ALPHA, GL_SRC_ALPHA);
glUniform3i(glGetUniformLocation(progID, "dmp_TexEnv[1].srcRgb"), 
    GL_TEXTURE0, GL_FRAGMENT_PRIMARY_COLOR_DMP, GL_PREVIOUS);
glUniform3i(glGetUniformLocation(progID, "dmp_TexEnv[1].srcAlpha"), 
    GL_PREVIOUS, GL_PREVIOUS, GL_PREVIOUS);

/* LUT */
GLfloat qlut[3][512], lut[512];
int j, co;

GLuint lutids[6];
glGenTextures(6, lutids);
/* RR, RG, RB */
for (co = 0; co < 3; co++) {
    for (j = 0; j < 128; j++) {
        LN = (float) j/128.f;
        qlut[co][j] = calc_lamb(LN, co) + calc_wrap(LN, co);
    }
    for (j = 128; j < 256; j++) {
        LN = (float) (j - 256) /128.f;
        qlut[co][j] = calc_wrap(LN, co);
    }
    for (j = 0; j < 127; j++)
        qlut[co][j + 256] = qlut[co][j + 1] - qlut[co][j];
    qlut[co][127 + 256] = calc_lamb(1.f, co) + calc_wrap(1.f, co) - 
        qlut[co][127];
    for (j = 128; j < 255; j++)
        qlut[co][j + 256] = qlut[co][j + 1] - qlut[co][j];
    qlut[co][255 + 256] = qlut[co][0] - qlut[co][255];
}
glBindTexture(GL_LUT_TEXTURE0_DMP, lutids[0]);
glTexImage1D(GL_LUT_TEXTURE0_DMP, 0, GL_LUMINANCEF_DMP, 512, 0, 
    GL_LUMINANCEF_DMP, GL_FLOAT, qlut[0]);
glBindTexture(GL_LUT_TEXTURE1_DMP, lutids[1]);
glTexImage1D(GL_LUT_TEXTURE1_DMP, 0, GL_LUMINANCEF_DMP, 512, 0, 
    GL_LUMINANCEF_DMP, GL_FLOAT, qlut[1]);
glBindTexture(GL_LUT_TEXTURE2_DMP, lutids[2]);
glTexImage1D(GL_LUT_TEXTURE2_DMP, 0, GL_LUMINANCEF_DMP, 512, 0, 
    GL_LUMINANCEF_DMP, GL_FLOAT, qlut[2]);
/* D1 */
for (j = 0; j < 256; j++) lut[j] = calc_t((float) j / 255.9375f);
for (j = 0; j < 255; j++) lut[j + 256] = lut[j + 1] - lut[j];
lut[255 + 256] = calc_t(1.f) - lut[255];
glBindTexture(GL_LUT_TEXTURE3_DMP, lutids[3]);
glTexImage1D(GL_LUT_TEXTURE3_DMP, 0, GL_LUMINANCEF_DMP, 512, 0, 
    GL_LUMINANCEF_DMP, GL_FLOAT, lut);
/* D0 */
memset(lut, 0, sizeof(lut));
for (j = 0; j < 128; j++) lut[j] = beckmann((float)j / 128.f, 0.5f);
for (j = 128; j < 256; j++) lut[j] = 0.f;
for (j = 0; j < 127; j++) lut[j + 256] = lut[j + 1] - lut[j];
lut[127 + 256] = 1.f - lut[127];
for (j = 128; j < 256; j++) lut[j + 256] = 0;
glBindTexture(GL_LUT_TEXTURE4_DMP, lutids[4]);
glTexImage1D(GL_LUT_TEXTURE4_DMP, 0, GL_LUMINANCEF_DMP, 512, 0, 
    GL_LUMINANCEF_DMP, GL_FLOAT, lut);
/* FR */
memset(lut, 0, sizeof(lut));
for (j = 0; j < 256; j++) 
lut[j] = r_fresnel((float)j / 255.9375f, 2.f, 0.35f, 0.f);
for (j = 0; j < 255; j++) lut[j + 256] = lut[j + 1] - lut[j];
    lut[255 + 256] = r_fresnel(1.f, 2.f, 0.35f, 0.f) - lut[255];
glBindTexture(GL_LUT_TEXTURE5_DMP, lutids[5]);
glTexImage1D(GL_LUT_TEXTURE5_DMP, 0, GL_LUMINANCEF_DMP, 512, 0, 
    GL_LUMINANCEF_DMP, GL_FLOAT, lut);

The calc_lamb, calc_wrap, and calc_t() functions are the functions used to calculate r(L・N), W(L・N), and T(L・N) respectively.

The reflection model is represented by the RR, RG, RB, and D1 lookup tables. D0 represents the effect of specular light and FR represents the effect of peripheral light.

Combiner 0 sums the object's primary color with the secondary color calculated by the reflection model. Specify GL_MULT_ADD_DMP for the color operand in combiner 1 to calculate and combine the surrounding lighting effects with the object's color.

5.6. Toon Shading

Toon shading is a way of shading where only two or three colors are used, creating blocks of color instead of smooth gradations. To apply toon shading, we use a function that outputs set values corresponding to specific resulting ranges of the dot products N・H or L・N. We use this step function to generate a lookup table whose outputs are stepped values. By multiplying the lookup table outputs by constant colors, we can shade a scene with clearly defined flat colors, much like a cartoon.

If changes in shading are caused solely by changes in the position of the lights, use the dot product L・N (GL_LIGHT_ENV_LN_DMP). If changes in the line of sight are also a factor, use the dot product N・H (GL_LIGHT_ENV_NH_DMP).

If you only plan to change the brightness of the specular 1 value of the lights, set the stepped values in the reflection (RR) lookup table and simply specify a layer configuration where each component of the reflection uses that lookup table. Alternately, using separate lookup tables for each component makes it possible to express extreme changes in coloring, even when the color step function is the same in each table.

You can render the outlines of a toon-shaded object using only specular light by rendering areas darkly (in black) where the orientations of the view vector and the normal differ greatly. Do this by taking the dot product N・V (GL_LIGHT_ENV_NV_DMP) as the input and setting up the distribution factor lookup tables (D0, D1) to yield either 0.0 (for all inputs close to 0.0) or 1.0 (for all other inputs).

Code 5-7. Toon Shading
glUniform1i(glGetUniformLocation(progID, "dmp_LightEnv.config"), 
        GL_LIGHT_ENV_LAYER_CONFIG2_DMP);
glUniform1i(glGetUniformLocation(progID, "dmp_LightEnv.lutEnabledRefl"), 
        GL_TRUE);
glUniform1i(glGetUniformLocation(progID, "dmp_LightEnv.absLutInputD0"), 
        GL_TRUE);
glUniform1i(glGetUniformLocation(progID, "dmp_LightEnv.absLutInputD1"), 
        GL_TRUE);
glUniform1i(glGetUniformLocation(progID, "dmp_LightEnv.absLutInputRR"), 
        GL_FALSE);
glUniform1i(glGetUniformLocation(progID, "dmp_LightEnv.lutInputRR"), 
        GL_LIGHT_ENV_LN_DMP);
glUniform1i(glGetUniformLocation(progID, "dmp_LightEnv.lutInputD0"), 
        GL_LIGHT_ENV_LN_DMP);
glUniform1i(glGetUniformLocation(progID, "dmp_LightEnv.lutInputD1"), 
        GL_LIGHT_ENV_NV_DMP);
glUniform1i(glGetUniformLocation(progID, 
        "dmp_FragmentLightSource[0].geomFactor0"), GL_FALSE);
glUniform1i(glGetUniformLocation(progID, 
        "dmp_FragmentLightSource[0].geomFactor1"), GL_FALSE);
glUniform1i(glGetUniformLocation(progID, 
        "dmp_FragmentLightSource[0].spotEnabled"), GL_FALSE);
glUniform1i(glGetUniformLocation(progID, "dmp_LightEnv.clampHighlights"), 
        GL_FALSE);

float delta[] = {1.0f, 0.7f, 0.5f, -1.0f};
for (i = 0, j = 127; j >= 0; j--) {
    LN = (float) j / 128.0f ;
    if (LN > delta[i]) lut[j] = previous;
    else {
        lut[j] = LN; previous = lut[j]; i++;
    }
}
for (j = 0 ; j < 127; j++) lut[j + 256] = lut[j+1] - lut[j];
lut[127 + 256] = 0.0f;
for (i = 0, j = 255; j >= 128; j--) {
    LN = (float)(j - 256) /128.0f;
    if (LN > delta[i]) lut[j] = previous;
    else {
        lut[j] = LN; previous = lut[j]; i++;
    }
}
for (j = 128; j < 255; j++) lut[j + 256] = lut[j+1] - lut[j];
lut[255 + 256] = lut[0] - lut[255];
glBindTexture(GL_LUT_TEXTURE0_DMP, lutids[0]);
glTexImage1D(GL_LUT_TEXTURE0_DMP, 0, GL_LUMINANCEF_DMP, 512, 0,
        GL_LUMINANCEF_DMP, GL_FLOAT, lut);
glUniform1i(glGetUniformLocation(progID, "dmp_FragmentMaterial.samplerRR"), 0);

float highlight_eps = 0.01f;
for (j = 0; j < 256; j++)
    if ((float) j / 256.0f <= 1.0f - highlight_eps) lut[j] = 0.0f;
    else                                            lut[j] = 1.0f;
for (j = 0; j < 255; j++) lut[j + 256] = lut[j+1] - lut[j];
lut[255 + 256] = 0.0f;
glBindTexture(GL_LUT_TEXTURE1_DMP, lutids[1]);
glTexImage1D(GL_LUT_TEXTURE1_DMP, 0, GL_LUMINANCEF_DMP, 512, 0,
        GL_LUMINANCEF_DMP, GL_FLOAT, lut);
glUniform1i(glGetUniformLocation(progID, "dmp_FragmentMaterial.samplerD0"), 1);

float outline = 0.15f;
for (j = 0; j < 256; j++)
    if ((float) j / 256.0f < outline) lut[j] = 0.0f; 
    else                              lut[j] = 1.0f; 
for (j = 0; j < 255; j++) lut[j + 256] = lut[j+1] - lut[j];
lut[255 + 256] = 1.0f - lut[255];
glBindTexture(GL_LUT_TEXTURE2_DMP, lutids[2]);
glTexImage1D(GL_LUT_TEXTURE2_DMP, 0, GL_LUMINANCEF_DMP, 512, 0, 
        GL_LUMINANCEF_DMP, GL_FLOAT, lut);
glUniform1i(glGetUniformLocation(progID, "dmp_FragmentMaterial.samplerD1"), 2);

As shown in the sample, delta is the array of dot products that produce stepped results, highlight_eps is the range of dot products taken as highlights, and outline is the range of dot products taken as shadows. Use these values to change how toon shading is rendered.


CONFIDENTIAL