Photo-Realistic Real-Time Face Rendering Semester Project LGG Laboratory, EPFL Daniel Chappuis
Photo-Realistic Real-Time Face Rendering Semester Project LGG Laboratory, EPFL Daniel Chappuis
Daniel Chappuis
1 Introduction 3
1.1 Previous work . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3
2 Real-time Skin Rendering 5
2.1 Theory of subsurface scattering . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5
2.1.1 Skin Surface Reectance . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5
2.1.2 Skin Subsurface Reectance . . . . . . . . . . . . . . . . . . . . . . . . . . . 6
2.1.3 Diusion Proles . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 7
2.1.4 Approximating Diusion Proles . . . . . . . . . . . . . . . . . . . . . . . . 8
2.2 Skin Rendering Algorithm . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 8
2.2.1 Texture-Space Diusion . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 9
2.2.2 Overview of the algorithm . . . . . . . . . . . . . . . . . . . . . . . . . . . . 9
2.2.3 Rendering Irradiance in Texture-Space . . . . . . . . . . . . . . . . . . . . . 10
2.2.4 Blurring the Irradiance Texture . . . . . . . . . . . . . . . . . . . . . . . . . 10
2.3 Specular lighting . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 15
2.4 Shadows . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 17
2.5 Modied Translucent Shadow Map . . . . . . . . . . . . . . . . . . . . . . . . . . . 20
2.6 Energy conservation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 23
2.7 Gamma correction . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 24
2.8 The Final Skin Shader . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 25
3 Environment lighting 28
3.1 Spherical harmonics . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 28
3.1.1 Properties of Spherical Harmonics . . . . . . . . . . . . . . . . . . . . . . . 30
3.2 Spherical harmonics for environment lighting . . . . . . . . . . . . . . . . . . . . . 30
3.3 Spherical harmonics and occlusions . . . . . . . . . . . . . . . . . . . . . . . . . . . 31
3.4 Rotation of Spherical Harmonics Coecients . . . . . . . . . . . . . . . . . . . . . 33
4 Results 35
4.1 Conclusion . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 35
4.2 Future work . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 35
4.3 Rendered images . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 36
A The Skin Rendering Application 40
A.1 About the Application . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 40
A.2 How to use the application . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 40
2
Chapter 1
Introduction
Skin rendering is a really important topic in Computer Graphics. A lot of virtual simulations or
video games contain virtual humans. In order to obtain a realistic realism of their faces, we need
to take care of skin rendering particularly because we are very sensitive to the appearance of skin.
Nowadays, we can use modern 3D scanning technology to obtain very detailed meshes and textures
for the face. But the diculty with skin rendering is that we need to model subsurface scattering
eects. Subsurface scattering is the fact that light goes under the skin surface, then scatters, gets
partially absorbed, and at the end exits the skin somewhere else. It is very important to correctly
handle this eect in order to render photo-realistic faces. There already exists oine techniques
that simulate skin subsurface scattering and give very realistic looking skin. But this has a cost,
it can take second or minutes to render.
With their work, Eugene d'Eon, David Luebke, and Eric Enderton [7] introduced a technique
to approximate subsurface scattering of skin in real-time on the GPU. They have obtained very
realistic results.
The goal of this project is to implement their algorithm in order to simulate subsurface scat-
tering of skin. I will also compare the results with dierent parameters of the algorithm. I have
also implemented diuse environment lighting with occlusions.
Then, in 2007 with their work [7], Eugene d'Eon, David Luebke and Erir Enderton used an
approximation of the scattering diusion proles of Donner and Jensen with a weighted sum of
six Gaussian functions. Because of the nice properties of Gaussian lters, they have been able to
render very realistic human skin in real-time on the GPU. You can see the result in gure 1.2.
Environment maps are usually used in Computer Graphcis to render a reective surface in
order that it reects the surrounding environment. But this is all about specular reection and it
is much more dicult to use environment map for diuse reection because at each point on the
surface, we need to compute diuse reection for every possible directions from the environment
map. Therefore, we need to compute an integral for each point on the surface which we cannot do
3
Figure 1.1: Skin rendering obtained by Donner and Jensen
Figure 1.2: Real-time skin rendering obtained by d'Eon, Luebke and Enderton
eciently in real-time. But in 2001, with their work [17], Ravi Ramamoorthi and Pat Hanrahan
introduced a way to approximate diuse lighting from an environment map with spherical har-
monics. After a precomputation of the environment map, it is possible to compute diuse lighting
from the environment map in real-time.
4
Chapter 2
This chapter is about the main part of the project which is the algorithm for rendering skin
subsurface scattering.
As you can see, taking subsurface scattering into account is very important in order to have a
realistic skin appearance. Subsurface scattering is the process where light goes beneath the surface
of the skin, scatters and gets partially absorbed, and then exits the skin surface somewhere else.
This results in a translucent appearance of the skin.
Now, we will try to understand how light interacts with skin. I will rst discuss the reectance
of light at the surface of skin and then the subsurface reectance.
5
Figure 2.2: Skin with subsurface scattering
Approximately 6 percent of the incident light reects directly at the surface of skin without
being colored. This is the result from a Fresnel eect with the topmost layer of the skin which
is quite oily. This reection is not a perfect mirror reection because the surface of skin is also
rough (as you can see on the right of gure 2.3). Therefore, an incident ray is not reected in only
one single direction. The reectance at the surface can be modeled using a specular bidirectional
reectance distribution function (BRDF) quite often used in Computer Graphics. But we cannot
use a simple model like Blinn-Phong because is it not physically-based and does not accurately
approximate the specular reection of skin. For this reason, we will use a physically-based specular
BRDF. This function will be explained in section 2.3.
It is important to understand that light is not absorbed and scattered the same way in the
dierent layers (epidermis and dermis) of the skin (see gure 2.4). The model that we are using
here is composed of three layers (oily surface layer, epidermis and dermis). Actually, real skin is
much more complex. Indeed, as explained in [14], the epidermis layer contains ve dierent layers
by itself. But Donner and Jensen in [3] have shown that a single-layer model is not sucient for
skin rendering and using a three-layer model seems to be a good choice. Note that the algorithm
for subsurface scattering that we will use, doesn't handle single scattering eects. Single scattering
6
is the fact that each ray scatters beneath the surface but only once. This is a quite correct
approximation for skin but if we would render marble or smoke for instance, it wouldn't be a good
choice because for those materials, single scattering is really important.
7
In 2005, Donner and Jensen presented their three-layer skin model and created diusion proles
predicted by their model.
For instance, the gure 2.6 shows the diusion prole of the gure 2.5 approximated by a sum
of two or four Gaussian functions. But why choosing a sum of Gaussian functions ? Obviously, it is
because of the very nice properties of Gaussian functions. Gaussian kernel is separable and radially
symmetric and moreover convolving Gaussian functions with each other produces new Gaussian
functions. Those properties are very useful mainly for eciency reasons.
In [7], they have found that six Gaussian functions were needed to correctly approximate the
three-layer skin model given in [3]. The gure 2.7 shows the parameters of those six Gaussian
functions with the corresponding weights and the gure 2.8 plots the corresponding approximation
of the diusion proles.
Note that the Gaussian weights of each prole sum up to 1. Therefore by normalizing these
proles to have a white diuse color, we make sure that the result after scattering will be white
on average. Then we can use an albedo texture color map to dene the color of skin.
8
Figure 2.7: Six Gaussian functions to approximate a three-layer skin model
In [7], they also used texture-space diusion to implement the six Gaussian convolutions. They
have also incorporated transmission through thin regions like ears and have used stretch correction
to obtain a more precise texture-space diusion.
9
in the temporary buer and keep the resulting texture. After this step, we have six dierent
blurred versions of the o-screen irradiance texture.
5. Render the mesh in 3D. We compute here the weighted sum of the six irradiance textures to
approximate for subsurface scattering at each pixel. We also add the specular reectance at
this point.
The code for the irradiance texture rendering is given in the irradianceShader vertex and
fragment shaders. This shader also implement energy conservation which will be descibed in
section 2.6. The gure 2.9 illustrates the o-screen irradiance texture computed for our mesh.
10
k
! k
X X
I∗ wi G(vi , r) = wi I ∗ G(vi , r)
i=1 i=1
Therefore, what we have to do is to convolve the irradiance texture I independently with each
of those six Gaussian kernels G(vi , r) and at the end compute the weighted sum of the resulting
textures to simulate subsurface scattering according to the skin diusion proles. To correctly
match the diusion proles of skin we have to use the correct weights given in gure 2.7 when
computing the weighted sum of convolded irradiance textures. Also note that the smallest Gaus-
sian is so narrow that using it to convolve the irradiance texture will not make any dierence.
Therefore we use the initial irradiance texture as the convoled version with the smallest Gaussian
kernel. Thus, we only need to compute ve other convolved irradiance textures. The gure 2.10
shows the irradiance texture of gure 2.9 convolved with the largest Gaussian kernel.
Figure 2.10: Irradiance texture convolved with the largest Gaussian kernel
A second nice property of Gaussian kernels is that they are separable. Therefore when comput-
ing the convolution of an image with a Gaussian kernel, we don't have to compute a 2D convolution
which is quite expensive if the kernel and the image are large. We only need to compute a 1D
convolution in the U direction and store the result into a temporary image and then compute
another 1D convolution in the V direction of that temporary image. This reduces a lot the number
of operations needed for convolving a 2D image.
The third property of Gaussians that we will use is that the convolution of two Gaussian func-
tions is also a Gaussian. Therefore, we can produce each convolved irradiance texture by convolving
the result of the previous one, allowing us to convolve the irradiance texture by wider and wider
Gaussian kernels without increasing the number of taps at each step.
The result of convolving two Gaussian functions G(v1 , r) and G(v2 , r) with variances v1 and v2
is the following :
Z ∞ Z ∞ p p
G(v1 , r)∗G(v2 , r) = G v1 , x02 + y 02 G v2 , (x − x0 )2 + (y − y 0 )2 dx0 dy 0 = G(v1 +v2 , r)
0 0
with r = x2 + y 2 . Therefore, if the previous convolved irradiance texture contains the con-
p
volved version I1 = I ∗ G(v1 , r) (note that I is the irradiance texture) and we want to compute
11
I2 = I ∗ G(v2 , r), we only need to convolve I1 with G(v2 − v1 , r).
We use a separable seven-tap convolution in each U and V direction. The blurring is done in
the blurShader vertex and fragment shaders. The same shader is used for the blurring in both U
and V direction and take as a parameter the blurring direction. We begin to convolve in the U
direction and store the result in a temporary texture and then convolve in the V direction. The
seven Gaussian tap weights are the following :
The listing 2.1 shows how the stretch-map texture is computed. First, we compute the texture-
space derivatives of world-space coordinates derivu and derivv in both U and V direction. Then
we invert those values and multiply by a stretchscale value in order that the inverted value is
in the range [0, 1] to be stored in a RGB texture.
// Compute t h e w o r l d c o o r d i n a t e c h a n g e s
vec3 d e r i v u = dFdx ( w o r l d C o o r d ) ;
vec3 d e r i v v = dFdy ( w o r l d C o o r d ) ;
12
// Compute t h e step distance
vec2 step = scaleConv ∗ blurDirection ∗ stretch ∗ gaussianStd / stretchScale ;
// Tap 0
vec4 tap0 = texture2D ( inputTexture , coords ) ;
sum += t a p 0 ∗ w e i g h t s [ 0 ] ;
c o o r d s += s t e p ;
// Tap 1
vec4 tap1 = texture2D ( inputTexture , coords ) ;
sum += t a p 1 ∗ w e i g h t s [ 1 ] ;
c o o r d s += s t e p ;
// Tap 2
vec4 tap2 = texture2D ( inputTexture , coords ) ;
sum += t a p 2 ∗ w e i g h t s [ 2 ] ;
c o o r d s += s t e p ;
// Tap 3
vec4 tap3 = texture2D ( inputTexture , coords ) ;
sum += t a p 3 ∗ w e i g h t s [ 3 ] ;
c o o r d s += s t e p ;
// Tap 4
vec4 tap4 = texture2D ( inputTexture , coords ) ;
sum += t a p 4 ∗ w e i g h t s [ 4 ] ;
c o o r d s += s t e p ;
// Tap 5
vec4 tap5 = texture2D ( inputTexture , coords ) ;
sum += t a p 5 ∗ w e i g h t s [ 5 ] ;
c o o r d s += s t e p ;
// Tap 6
vec4 tap6 = texture2D ( inputTexture , coords ) ;
sum += t a p 6 ∗ w e i g h t s [ 6 ] ;
The other possibility is to multiply by the diuse albedo color in the nal pass after the sub-
surface scattering convolutions and not when computing the irradiance texture. This is called
13
Figure 2.11: Subsurface scattering with only pre-scatter texturing (without specular reectance)
Post-Scatter texturing. With this technique, the subsurface scattering convolutions is done with-
out any colors. Now, high-frequency details are kept because their are not blurred by the subsurface
scattering convolutions. Again, according to [7], the problem with this method is that there is no
color bleeding of the skin tones. The gure 2.12 shows an image of our human head with only
post-scatter texturing. We can observe that the high-frequency details are kept.
Figure 2.12: Subsurface scattering with only post-scatter texturing (without specular reectance)
A good solution is to combine pre-scatter and post-scatter texturing. With this technique, a part
of the diuse albedo color is applied in the irradiance texture computation (pre-scatter texturing)
and the remaining part is applied at the end after the subsurface scattering computation (post-
14
scatter texturing). To implement this, we can multiply the diuse irradiance diffuseIrradiance
by a portion of the diuse albedo diffuseAlbedo color in the irradiance texture computation as
follows :
diffuseIrradiance ∗ = pow(diffuseAlbedo, mixRatio)
where mixRatio is a value in the range [0, 1] representing the amount of pre-scatter or post-
scatter texturing. The previous multiplication is implemented in the irradianceShader fragment
shader. Then, we also have to apply the remaining part of the diuse albedo color at the end in
the nal pass (after the subsurface scattering computation). We implement this as follows :
In general, a BRDF function fBRDF (ωi , ωo ), where ωi and ωo are respectively the input and
output directions of the light, is dened as follows at a given surface point :
dLr (ωo ) dLr (ωo )
fBRDF (ωi , ωo ) = = (2.1)
dEi (ωi ) Li (ωi ) cos θi dωi
where L is the output radiance, E is the irradiance and θi is the angle between the normal at
the given surface point and ωi . Therefore, we have :
Note that the term dot(N, L) comes from the denition of the BRDF (see the cosine term in
equation 2.2).
The Fresnel eect is the fact that the specular reectance increases at grazing angles. It is
important to take this eect into account if we want to obtain a physically plausible result. As
in [6], we use the Schlick's Fresnel approximation (see [4]) that seems to works quite well for
skin. The listing 2.3 shows the code to compute the Fresnel term. This code can be found in
the finalSkinShader fragment shader. Note that H is the half-viewing vector, V is the viewing
vector and F 0 is the reectance at normal incidence. As explained in [6], the value 0.028 should
be used for skin.
15
// Compute t h e Fresnel reflectance for specular lighting
f l o a t f r e s n e l R e f l e c t a n c e ( v e c 3 H, v e c 3 V, f l o a t F0 ) {
f l o a t b a s e = 1 . 0 − d o t (V, H) ;
f l o a t e x p o n e n t i a l = pow ( b a s e , 5 . 0 ) ;
return e x p o n e n t i a l + F0 ∗ ( 1 . 0 − e x p o n e n t i a l ) ;
}
where m is the roughness of the material and H is the half-vector bewteen L and V .
Heidrich and Seidel described in 1999 a precomputation strategy to eciently evaluate a BRDF
model (see [18]). The idea was to factor the BRDF into several precomputed 2D textures. In [6],
the used a similar method but precomputed a unique texture corresponding to the Beckmann
distribution function that we have just seen and used the Schlick Fresnel approximation that we
have introduced above. Their precomputation of the Beckmann distribution is done by rendering a
texture that will be accessed by the dot product of N and H and the roughness m. The listing 2.4
shows how the Beckmann texture is precomputed. This code can be found in the beckmannShader
fragment shader.
void main ( ) {
// Compute and map t h e PH v a l u e in the range [0 , 1] to be s t o r e d in the
texture
f l o a t PH = 0 . 5 ∗ pow ( PHBeckmann ( t e x C o o r d . x , texCoord . y ) , 0.1) ;
g l _ F r a g C o l o r = v e c 4 (PH, PH, PH, 1 . 0 ) ;
}
// Compute t h e Beckmann PH v a l u e
f l o a t PHBeckmann ( f l o a t ndoth , f l o a t m) {
f l o a t a l p h a = a c o s ( ndoth ) ;
f l o a t ta = tan ( alpha ) ;
f l o a t v a l = 1 . 0 / (m∗m∗ pow ( ndoth , 4 . 0 ) ) ∗ exp ( − ( t a ∗ t a ) / (m∗m) ) ;
return v a l ;
}
16
float ndotl = d o t (N, L ) ;
i f ( ndotl > 0.0) {
vec3 h = L + V;
vec3 H = normalize ( h ) ;
f l o a t ndoth = d o t (N, H) ;
f l o a t PH = pow ( 2 . 0 ∗ t e x t u r e 2 D ( beckmann_texture , v e c 2 ( ndoth , m) ) . r ,
10.0) ;
f l o a t F = f r e s n e l R e f l e c t a n c e ( H, V, 0 . 0 2 8 ) ;
f l o a t f r S p e c = max ( PH ∗ F / d o t ( h , h ) , 0 ) ;
s p e c u l a r = n d o t l ∗ rho_s ∗ f r S p e c ;
}
return specular ;
}
Figure 2.13: Rendering with roughness m = 0.1. Right image is only specular
2.4 Shadows
Obviously, simulating shadows is very important for realistic rendering. As we have already said
before, we use shadow mapping to render shadows. Shadow mapping is a classical but widely-used
technique. A surface point of our object is not in shadow if there is no other object between the
point and a certain light source. The idea of shadow mapping is to place the camera at the light
source position and render the scene from this point but instead of rendering the pixel color of the
objects of the scene, we render the distance dshadow of each pixel to the light source. This rendering
is stored into a texture which is called a shadow map. Therefore, the shadow map contains the
distances to the light source of all objects visible from that light source. Then when rendering a
given pixel p of our object from the camera view, we compute the distance dpixel from that pixel
p to the light source. Then we lookup in the shadow map the distance dshadow in the shadow map
corresponding to the direction of the pixel p from the light source. Then we compare both distances
and if dpixel > dshadow , it means that the pixel p is in shadow and cannot receive light from that
light source. The gure 2.16 shows the dierence when rendering without and with shadows.
17
Figure 2.14: Rendering with roughness m = 0.3. Right image is only specular
Figure 2.15: Rendering with roughness m = 0.5. Right image is only specular
We need to generate a shadow map for each light source of the scene. This is done at the
beginning of each frame. Then when computing the irradiance texture, we use the shadow maps
to compute a shadow factor for each pixel and light source. The shadow factor for a given pixel
and a given light source tells us if the light source contributes to the irradiance of that pixel.
Shadow mapping is not so easy because it can generate serious artifacts. Firstly, we need to
take care of self-shadowing. Self-shadowing occurs because the comparison between dpixel and
dshadow is a oating number comparison and therefore because of the oating number precision, it
cannot always get the right answer if two values are very close. This can cause Moiré artifacts as
shown in the left image of gure 2.17. A possible solution to this problem is to add a small bias
for instance using the glPolygonOffset() function of OpenGL.
A second problem with shadow mapping is the generation of hard shadows and aliased shadow
edges (as you can see in the right image of gure 2.17) To correct this problem we implement a
18
Figure 2.16: Rendering without shadows (left) and with shadows (right)
Figure 2.17: Shadow mapping artifacts (Moiré patterns) due to self-shadowing (left) and aliased
shadow edges (right)
technique called Percentage Closer Filtering (PCF). The idea is to smooth the aliased edges by
simply sampling several surrounding texels in the shadow map along with the center texel and
take the average of all the shadow factors. I use a 4x4 PCF kernel which means I sample the 16
surrounding texels of the texel in the shadow map I want to compute the shadow factor. The listing
2.6 shows the code that implement the PCF technique. Notice that we need to compute a shadow
factor in the irradianceShader fragment shader to take care of shadows for diuse lighting but
also in the finalSkinShader fragment shader to obtain shadows for the specular reectance term.
19
}
return shadow ;
}
Now, we will see how to compute global scattering given that we have the modied translucent
shadow map corresponding to a light source. If you look at the gure 2.19, the goal is to com-
pute at any shadowed surface point C global subsurface scattering using the part of the convolved
irradiance texture around the point A that is in the light-facing surface. For each point C , the
20
Translucent Shadow Map contains the distance m and the U V texture coordinate of point A.
The scattered light that exits point C is the convolution of the light-facing points by the diusion
prole R, where distances from point C to each sample are computed individually. But instead,
we compute this for the point B because it is easier and for small angles q , B is close to C . If q
is large, the Fresnel and N ·L factor will make the contribution to be quite small and hide the error.
In order to compute scattering at point B , any sample at distance r away from point A on the
light-facing surface has to be convolved by the following :
k k
−d2
p X p X
R( r2 + d2 ) = wi G vi , r2 + d2 = wi e vi G(vi , r)
i=1 i=1
This formula is very useful because we know that the points on the light-facing surface have
already been convolved with G(vi , r) (it is what is stored in the convolved irradiance textures).
Therefore the total diuse light exiting at point C is a sum of k texture lookups (using the texture
coordinates (u,v) of point A in the Translucent Shadow Map), each weighted by the weights wi
−d2
and the exponential term e vi . Note that the depth m computed from the Translucent Shadow
Map is corrected by the factor cos(q) because we use a diusion approximation and the most di-
rect thickness is more applicable. We also compare surface normals at points A and C and this
correction is only applied if the normal are in opposite directions. We use linear interpolation to
blend this correction. To avoid high-frequency artifacts in the Translucent Shadow Map, we store
the depth through the surface in the alpha channel of the irradiance texture. Then when blurring
the irradiance texture during convolution, the alpha channel is also blurred resulting in a blurred
version of depth at the end. Then, we use the correct version of depth (the correct convolved
irradiance texture) when we compute global scattering. For instance, we use the alpha channel of
convolved irradiance texture i − 1 for the depth when we compute the Gaussian i. Note that we
store e(−const·d) in the alpha channel in order that the thickness d can be stored in a 8-bit channel.
When the texture coordinates (u,v) of the point beeing rendered approaches the texture coor-
dinates of the point in the Translucent Shadow Map, double contribution to subsurface scattering
can occur. We use linear interpolation such that the Translucent Shadow Map contribution is zero
21
for each Gaussian term when the two texture coordinates approach each other. The listing 2.7
shows the code of the irradianceShader fragment shader that compute the thickness through the
skin and store it into the alpha channel of the irradiance texture.
Listing 2.7: Code for computing the thickness through skin and storing it into the texture
The code that computes the part of the global scattering using the thickness through the
skin is in the finalSkinShader fragment shader and you can see that code in the listing 2.11.
The gure 2.20 shows the result of rendering with global scattering using Modied Translucent
Shadow Map. Note that in our application there are three positional light sources but I have only
applied Translucent Shadow Map for the rst light source. The Translucent Shadow Map can
contains some artifacts due to the borders of the 3D mesh (faces that are at a 90 degrees angle
with the light direction) where the thickness through the skin is very close to zero. Such artifacts
can be seen sometimes depending on the positon of the light source and the orientation of the mesh.
22
2.6 Energy conservation
In the skin rendering algorithm, specular and diuse lighting are treated completely independently.
But this could be a problem because the total light energy leaving the surface at a given point
can be larger than the incoming light energy. This would not be physically realistic. Therefore we
need to take care of energy conservation in the algorithm.
The energy available for the subsurface scattering is exactly all the energy not reected by the
specular BRDF. Therefore, before computing the diuse lighting and storing it in the irradiance
texture, we need to compute the total specular reectance at this surface point and multiply the
diuse light by the fraction of energy that remains. Note that we need to integrate the specular
reectance over all the hemisphere to take care of all the viewing directions V . Therefore, if we
consider that fr (x, ωo , L) is the specular BRDF, L is the light vector at surface point x and ωo is a
viewing direction in the hemisphere about the normal N , then the diuse light will be attenuated
by the following factor for each light :
Z
ρdt (x, L) = 1 − fr (x, ωo , L)(ωo · N )dωo
2π
Note that this value will change depending on the roughness m at surface point x and on the
dot product N · L. The previous integral is precomputed for all combinations of roughness values
and angles and is stored in a 2D texture to be accessed in the irradianceShader fragment shader
based on m and N · L. Note that this precomputation is an approximation of the integral. This
precomputation is peformed in the energyConservation fragment shader and the code is shown
in the listing 2.8. Note that the ρs from the specular BRDF is not taken into account in the
precomputation because it will be applied later on.
int numterms = 8 0 ;
vec3 N = vec3 ( 0 . 0 , 0.0 , 1.0) ;
vec3 V = vec3 ( 0 . 0 , sqrt (1.0 − costheta ∗ costheta ) , costheta ) ;
sum += l o c a l s u m ∗ ( p i / 2 . 0 ) / f l o a t ( numterms ) ;
}
f l o a t r e s u l t = sum ∗ ( 2 . 0 ∗ p i ) / ( f l o a t ( numterms ) ) ;
gl_FragColor = vec4 ( r e s u l t , r e s u l t , r e s u l t , 1 . 0 ) ;
23
Now that we have a texture that gives us the term ρdt , we can use it in the irradianceShader
fragment shader to attenuate the irradiance that will be used for subsurface scattering by using only
the energy that remains. We have to do this for each light source. The corresponding code is in the
irradianceShader fragment shader and is shown in the listing 2.9. Note that the specIntensity
corresponds to the ρs factor from the specular BRDF.
Usually, this behavior is called Gamma behavior. Note that LCD screens don't inherently have
this property but they are constructed to mimic the behavior of a CRT screen.
To avoid that this gamma behavior changes the constrast of our rendered images, we need to
correct for it. The basic idea is to apply the inverse of this gamma behavior to our nal images
just before that they are sent to the screen. Therefore, we have to apply the following function to
the intensities x of our image before sending it to the screen :
1
ycorrect (x) = x γ
24
Figure 2.21: Non-linear behavior of a CRT screen and Gamma correction
This is called Gamma correction. If we apply this correction to our nal image before sendind
it to the screen, the resulting output will be linear and therefore, the contrasts in our image will be
preserved. I have applied this gamma correction at the end of the finalSkinShader (see listing
2.11).
But this is not all, we have to know that a lot of image le (like JPEG for instance) are
precorrected (with a gamma of 2.2) in order that a user doesn't have to take care of gamma
correction by himself if he wants to look at the image on a screen. Therefore, intensity values
in such an image le are not linear. Therefore, we cannot directly use those value from an input
texture in our subsurface scattering algorithm because we would perform subsurface scattering in
a non-linear space. This would cause some problems as described in [13]. A common problem with
skin rendering, is the appearance of blue-green glow around the shadow edges. To avoid this eect,
we have to apply the inverse of gamma correction to our texture images before using them. For
instance, you can see in the irradianceShader and finalSkinShader fragment shaders, that we
apply the following transform y(x) before using the albedo texture :
y(x) = x2.2
25
// R e n o r m a l i z e diffusion profile to white
v e c 3 normConst = g a u s s 1 w + g a u s s 2 w + g a u s s 3 w + g a u s s 4 w + g a u s s 5 w + g a u s s 6 w ;
d i f f u s e /= normConst ;
// Energy c o n s e r v a t i o n
if ( isEnergyConservationActive ) {
float a tt e nu a ti o n = s p e c I n t e n s i t y ∗ texture2D ( energyAttenuationTexture ,
vec2 ( dot ( n , v ) , roughness ) ) . r ;
float f i n a l S c a l e = 1.0 − attenuation ;
d i f f u s e ∗= f i n a l S c a l e ;
}
// Compute s p e c u l a r reflectance
vec3 s p e c u l a r = vec3 ( 0 . 0 ) ;
if ( isSpecularActive ) {
i f ( i s L i g h t 0 A c t i v e ) s p e c u l a r += shadow [ 0 ] ∗ gl_LightSource [ 0 ] . s p e c u l a r . rgb
26
∗ KS_Skin_Specular ( n , L [ 0 ] , v , r o u g h n e s s , s p e c I n t e n s i t y ) ;
if ( i s L i g h t 1 A c t i v e ) s p e c u l a r += shadow [ 1 ] ∗ g l _ L i g h t S o u r c e [ 1 ] . s p e c u l a r . r g b
∗ KS_Skin_Specular ( n , L [ 1 ] , v , r o u g h n e s s , s p e c I n t e n s i t y ) ;
if ( i s L i g h t 2 A c t i v e ) s p e c u l a r += shadow [ 2 ] ∗ g l _ L i g h t S o u r c e [ 2 ] . s p e c u l a r . r g b
∗ KS_Skin_Specular ( n , L [ 2 ] , v , r o u g h n e s s , s p e c I n t e n s i t y ) ;
}
// Apply gamma c o r r e c t i o n
vec3 f i n a l C o l o r = pow ( d i f f u s e + s p e c u l a r , v e c 3 ( 1 . 0 / gamma) ) ;
27
Chapter 3
Environment lighting
Instead of using a certain number of light sources for illuminating a scene, it can be much more real-
istic to use environment lighting. The idea is to use the light comming from the whole environment
of the scene. Usually we can do this using environment mapping where we have a texture (a cube
or a sphere) that contains the light comming from each direction around the object. Environment
map are very often used for specular lighting for rendering objects that reect the environment.
The problem is that it is quite expensive to compute the diuse irradiance at a given point of the
surface of an object if the environment map is large.
Consider that we have an environment map with k texels. Each texel can be thought of beeing
a single light source. Therefore, for each surface point of the object the diuse component can be
computed as follows :
i=1,...,k
where Li is the light direction of texel i and N is the surface normal. It is obvious that if the
number of texels k is large computing this sum for each point of the surface of the object is really
expensive.
More generally, this is the same as computing the irradiance E with the integral over a upper
hemisphere Ω(N ) of light directions ω as follows :
Z
E(N ) = L(ω)(N · ω)dω (3.1)
Ω(N )
where N is the surface normal and L(ω) is the amount of light comming from the direction ω .
It is not possible to compute such an integral for surface point of our object in real-time.
We will use a technique using Spherical Harmonics to approximate the diuse lighting comming
from an environment map and to eciently compute the diuse irradiance at each surface point very
eciently. This technique has been introduced in 2001 by Ravi Ramamoorthi and Pat Hanrahan
with their work [17]. First, we will introduce the concept of spherical harmonics.
(3.2)
X
f˜(x) = ci bi (x)
i
28
where ci is the weight of the basis function bi . To nd those weights, we need to project the
original function f (x) onto each basis function bi (x). We can do this by computing the integrals :
Z
ci = f (x)bi (x)dx
Then by using equation 3.2, we can compute an approximation of the original function f (x).
But now, take a look at the equation 3.1 that computes the irradiance over a hemisphere. We can
consider that the function L(ω) is a function over the surface of the sphere. How can we use the
technique we have just seen with a 1D function to approximate a lighting function f (s) over a 2D
surface of a sphere S . To do this, we can use Spherical Harmonics. The spherical harmonics are
the basis functions over the surface of the sphere. Robin Green has written a very nice document
[11] that explains the theory of spherical harmonics.
Consider the standard parameterization of points on the surface of a unit sphere into spherical
coordinates :
x = sin θ cos φ
y = sin θ sin φ
z = cos θ
But we need to compute this integral numerically and therefore we can use Monte Carlo Inte-
gration. Monte Carlo integration allows us to compute the integral of a function f (x) as follows
: Z N
1 X
f (x)dx ≈ f (xj )w(xj )
N j=1
where N is the number of samples f (xj ) of the function f that we have and w(xj ) is given by :
1
w(xj ) =
p(xj )
where p(x) is the probability distribution function of the samples.
Therefore, if we consider again the equation 3.3, we have, using spherical coordinates :
Z 2π Z π
ci = f (θ, φ)yi (θ, φ) sin θdθdφ
0 0
29
If we choose the samples for Monte Carlo Integration such that they are unbiased w.r.t. surface
on the sphere, each sample has equal probability of appearing anywhere on the surface of the
sphere which gives us a probability function :
1
p(xj ) =
4π
Therefore, using Monte Carlo Integration, if we have the samples f (xj ), we can compute the
spherical harmonics coecients as follows :
N
4π X
ci = f (xj )yi (xj )
N j=1
Note that for a n-th order approximation we need n2 spherical harmonic coecients.
This is useful because it means that by using spherical harmonic functions we can guarantee
that when we animate scenes, move lights or rotate the objects, the intensity of lighting will not
uctuate or have any artifacts.
The other very nice property is that integrating the product of two spherical harmonic functions
is the same as evaluating the dot product of their coecients. Consider two spherical harmonic
functions f˜(s) and g̃(s), we have :
Z n
X
f˜(s)g̃(s)ds = fi gi
S i=0
where fi and gi are the spherical harmonics coecients of the functions f and g .
30
This is really useful because we can precompute only 9 spherical harmonics coecients oine
for a given environment map and at rendering time we just have to compute the equation 3.4 with
n = 3 which is much more ecient than evaluating an integral such as in equation 3.1 for each
surface point of the object.
In [17], they have computed the spherical harmonics coecients for a certain number of light
probes environment maps that can be found on http://ict.debevec.org/~debevec/Probes/.
Notice that each of the spherical harmonic coecient is separated in the three color channels
resulting in a total of 27 coecients for a given environment map.
where g(ω) = V (ω)(N · ω). Now, we can see that L is a function that depends only on the
environment map and that g is a function that depends only on the geometry of the mesh. Thus,
at each point of the surface of our object, we need to compute this integral of the product of the
functions L and g . But we have seen in section 3.1 that integrating the product of two spherical
harmonics functions is equivalent to evaluating the dot product of their spherical harmonics coef-
cients. Therefore, we can compute (by projection) the spherical harmonics coecients Li and gi
and then at rendering time, for each point of the surface of the object, we only have to compute :
Z n
(3.5)
X
E= L(ω)g(ω)dω = Li gi
S i=0
The coecients Li comes from the projection of the environment lighting function onto spher-
ical harnomics basis functions. They are the same 9 coecients of the previous section that can
be precomputed given an environment map.
We can compute the others spherical harmonics coecients gi by projecting the function g
onto the spherical harmonics basis function. Notice that this function is dierent at each point on
the surface of the object because it depends on the normal vector N and the visibility function
V (ω). Projecting the function g will also give us 9 spherical harmonics coecients gi . Therefore,
we need to compute those 9 coecients gi at each point of the surface of our object. What we can
do instead is to approximate this by computing those coecients only at each vertex of the mesh
(which seems to be a good approximation if our head model contains a large number of faces).
This projection is precomputed oine for our head object.
How can we precompute the 9 coecients gi for a given vertex of the mesh. Remember that
g(ω) is a function dened on the surface of a sphere for each vertex. Because we want to project
this function onto the spherical harmonics basis using Monte Carlo integration, we have to generate
some samples on the surface of the sphere. Then for each sample direction ωj , we need to evaluate
g(ωj ), which means we need to compute the dot product N · ωj where N is the normal of the
vertex and also we have to evaluate the visibility term V (ωj ) at the current vertex. To do this,
31
I have used the idea found in [15] that consist of placing the camera at the vertex position and
rendering the mesh into a cubemap (the cubemap is cleared in white and the mesh is rendered is
black). Thus, at each vertex, we have a way to evaluate the visibility term V (ωj ). We only have
to lookup in the cubemap in the direction ωj and if the cubemap texel is this direction is white
it means the vertex is not occluded in this direction and if it is black, the vertex is occluded by
another part of the mesh. For each vertex, and for each sample direction ωj we send the values N ,
ωj and the cubemap to a fragment shader that computes the evaluation of the sample g(ωj ). The
listing 3.1 shows the corresponding code that can be found in the shOcclusionShader fragment
shader.
Listing 3.1: Code to evaluate the funtion g(ωj ) at a given sample direction ωj
Now, we can evaluate the function g at each vertex in a certain number of sample directions
ωj which allows us to compute by Monte Carlo Integration the projection of the function g onto
spherical harmonics basis functions to get the 9 coecients gi at each vertex of the mesh.
Because the coecients Li and gi are precomputed oine, at rendering time, we only have
to compute, at each point of the surface of the object, the dot product of equation 3.5 to obtain
the diuse irradiance comming from the environment lighting. This is done when computing the
irradiance in the irradianceShader vertex shader. The listing 3.2 shows to corresponding code.
Notice that the lighting coecients Li contains color information and therefore we have one spher-
ical harmonic coecient Li per color channel which gives us in total 27 coecients. The spherical
harmonics coecients gi don't contain color information and therefore we have nine coecients
per vertex.
32
Figure 3.1: Environment lighting without occlusions (left) and with occlusions (right)
We can use the technique from [11], the idea is to represent our 9 spherical harmonics coecients
by a 9 × 1 vector C and to multiply it by a 9 × 9 rotation matrix RSH (α, β, γ) that depends on the
rotation Euler angles α, β and γ . We can decompose the RSH matrix using the ZY Z formulation.
It means that rst we rotate about the Z axis, then around the rotated Y axis and nally around the
rotated Z axis. With this formulation, we can generate rotations around any possible orientation.
The rotation of an angle β around the Y axis can be decomposed as a rotation X+90 of 90 degrees
around the X axis, a rotation Zβ of an angle β around the Z axis a a rotation X−90 of -90 degrees
around the X axis. Therefore, we have :
RSH (α, β, γ) = Zγ Yβ Zα = Zγ X−90 Zβ X+90 Zα
Therefore, we only need the three rotation matrices Z , X+90 and X−90 . The Z matrix can be
computed as follows given an angle θ :
1 0 0 0 0 0 0 0 0
0 cos(θ) 0 sin(θ) 0 0 0 0 0
0 0 1 0 0 0 0 0 0
0
− sin(θ) 0 cos(θ) 0 0 0 0 0
0
Zθ = 0 0 0 cos(2θ) 0 0 0 sin(2θ)
0 0 0 0 0 cos(θ) 0 sin(θ) 0
0 0 0 0 0 0 1 0 0
0 0 0 0 0 − sin(θ) 0 cos(θ) 0
0 0 0 0 − sin(2θ) 0 0 0 cos(2θ)
33
1 0 0 0 0 0 0 0 0
0 0 −1 0 0 0 0 0 0
0 1 0 0 0 0 0 0 0
0 0 0 1 0 0 0 0 0
0 0 0 0 0 0 0 −1 0
X+90 =
0 0 0 0 0 −1 0 0 0√
3
− 21
0
0 0 0 0 0 0 − 2
0 0 0 0 1 0 0√ 0 0
0 0 0 0 0 0 − 23 0 1
2
And the last matrix X−90 is the transposed matrix of X+90 . Therefore, given the three Euler
angles of rotation of our mesh α, β and γ , we can compute the rotation matrix RSH and multiply
it by the spherical harmonics coecients vector C to obtain a new 9 × 1 vector that contains the
rotated spherical harmonics coecients of the environment lighting. Notice that we should use the
fact that the matrices Z , X−90 and X+90 are quite sparse to make the rotation computation more
ecient.
34
Chapter 4
Results
4.1 Conclusion
I have implemented the skin rendering algorithm introduced in [7] on the GPU. It seems to work
quite well. There is still some artifacts with the global scattering using Modied Translucent
Shadow Map. The shadow mapping also wasn't so easy to implement because of the several arti-
facts that arise when using shadow maps. Finally, I think that the rendering images that I have
obtained are quite realistic.
During this project, I learned a lot about physically-based rendering. Especially about sub-
surface scattering, shadow mapping, translucent shadow maps and environment lighting using
spherical harmonics. I've also improved my skills with GPU programming.
As I have already said earlier, for the current application we use only a constant roughness
parameter m over the entire face. Instead, we can use a texture map in which the roughness
parameter is stored and is allowed to vary over the face. This would give a more realistic result
because it would be more physically plausible.
Texture seams can generate problems for texture-space diusion, because connected regions on
the 3D mesh are disconnected in the texture space and cannot easily diuse light between them.
Indeed, the empty regions on the texture will blur onto the mesh along each seam edge which
causes artifacts in the nal rendering. A solution to this problem is described in [6]. The idea is to
use a map or alpha value to detect when the irradiance textures are being accessed in a region near
a seam (or an empty space). When such a place is detected, the subsurface scattering computation
is turned o and the diuse reectance is replaced with an equivalent local computation. The
amount of artifacts depends on the texture of the mesh. It can be important to take care of the
seam artifacts because if they are strongly visible, the rendering will not look very realistic.
Another way to improve a lot the rendering would be to use High Dynamic Range (HDR)
rendering. With HDR rendering, the realism of the scene can be improved especially when the
scene contains for instance very glossy specular highlights. The current application is using the
GLUT library for managing the OpenGL window. Using another windows manager could allow us
to use more sample buers for multisampling. The more sample buers we use the better we can
deal with antialiasing of the scene. Therefore, by using more sample buers, we could increase the
quality of the scene of the application.
35
4.3 Rendered images
Here is some rendering that I have obtained with the application.
Figure 4.1: Rendering with three light sources and the environment lighting from the Uzi Gallery
light probe
36
Figure 4.2: Rendering with only one positional light source
Figure 4.3: Rendering with two light sources and the environment lighting from the Grace Cathedral
light probe
37
Figure 4.4: Rendering with two light sources and the environment lighting from the Uzi Gallery
light probe
Figure 4.5: Rendering with environment lighting from the Uzi Gallery light probe with a girl mesh
38
Figure 4.6: Rendering with two light sources and environment lighting from the Uzi Gallery light
probe
39
Appendix A
The parameters of the skin rendering algorithm like the roughness parameter m or the mixRatio
for pre-scatter texturing can be changed easily and can be found in the PhotoRealistic.hhp le.
The code for precomputing the spherical harmonics coecients for the occlusions at each vertex
can be found in the Mesh3D.cpp le.
where dataDirectory is the directory where the data needed by the application are stored
like the mesh, the textures, . . . The second parameter is optional and can be either y (yes) or n
(no). If this parameter is y, the spherical harmonics coecients for the occlusions for each vertex
are precomputed when the application starts and will be used later to render the scene while the
application is running. By default, those coecients are not computed at the beginning of the
application but are loaded from a le.
40
key d : move the light source 0 to the right around the vertical axis.
key w : move the light source 0 up around the horizontal axis.
key s : move the light source 0 down around the horizontal axis.
key e : enable/disable the environment lighting.
key z : enable/disable the specular lighting.
key o : enable/disable the shadows.
key b : enable/disable subsurface scattering.
key y : enable/disbale the Translucent Shadow Map for global scattering.
key h : rotate the mesh around the vertical axis.
key j : rotate the mesh around the horizontal axis.
mouse : use the mouse to rotate the camera around the mesh.
41
Bibliography
42