Real Time Skin Rendering

Real Time Skin Rendering
David Gosselin
3D Application Research Group
ATI Research, Inc.
Overview
•
•
•
•
•
•
Background
Texture space lighting
Spatially varying blur
Dilation
Adding shadows
Specular with shadows
Why Skin is Hard
• Most diffuse lighting from skin comes from
sub-surface scattering
• Skin color mainly from epidermis
• Pink/red color mainly from blood in dermis
• Lambertian model designed for “hard”
surfaces with little sub-surface scattering
so it doesn’t work real well for skin
Rough Cross Section
Air
Epidermis
Dermis
Bone, muscle, guts, etc.
Research
• There are several good mathematical
models available
• We looked at using Hanrahan/Krueger
(SIGGRAPH 93) based model
– Good but expensive for current technology
– Over 100 instructions per light
Basis for Our Approach
• SIGGRAPH 2003 sketch Realistic Human Face
Rendering for “The Matrix Reloaded” by
George Borshukov and J. P. Lewis
• Rendered a 2D light map
• Simulate subsurface diffusion in image domain
(different for each color component)
• Used traditional ray tracing for areas where light
can pass all the way through (e.g. ears)
• Also capture fine detail normal maps and albedo
maps
Texture Space Subsurface Scattering
• From Realistic Human
Face Rendering for
“The Matrix Reloaded”
@ SIGGRAPH 2003:
From Matrix: Reloaded sketch
• Our results:
Current skin in Real Time
Texture Space Lighting for Real Time
• Render diffuse lighting into an off-screen
texture using texture coordinates as position
• Blur the off-screen diffuse lighting
• Read the texture back and add specular
lighting in subsequent pass
• We only used bump map for the specular
lighting pass
Basic Approach
Light in
Texture Space
Blur
Geometry
Sample texture space light
Back
Buffer
Texture Coordinates as Position
• Need to light as a 3D model
but draw into texture
• By passing texture
coordinates as “position”
the rasterizer does the
unwrap
• Compute light vectors
based on 3D position and
interpolate
Texture Lighting Vertex Shader
VsOutput main (VsInput i)
{
// Compute output texel position
VsOutput o;
o.pos.xy = i.texCoord*2.0-1.0;
o.pos.z = 1.0;
o.pos.w = 1.0;
// Pass along texture coordinates
o.texCoord = i.texCoord;
// Skin
float4x4 mSkinning = SiComputeSkinningMatrix (i.weights, i.indices);
float4 pos = mul (i.pos, mSkinning);
pos = pos/pos.w;
o.normal = mul (i.normal, mSkinning);
// Compute Object light vectors
// etc.
. . .
Texture Lighting Pixel Shader
float4 main (PsInput i) : COLOR
{
// Compute Object Light 0
float3 vNormal = normalize (i.normal);
float3 lightColor = 2.0 * SiGetObjectAmbientLightColor(0);
float3 vLight = normalize (i.oaLightVec0);
float NdotL = SiDot3Clamp (vNormal, vLight);
float3 diffuse = saturate (NdotL * lightColor);
// Compute Object Light 1 & 2
. . .
float4 o;
o.rgb = diffuse;
float4 cBump = tex2D (tBump, i.texCoord);
o.a = cBump.a; // Save off blur size
return o;
}
Texture Lighting Results
Traditional Lighting
Texture Space Lighting
Rim light
• We wanted to further emphasize the light
that bleeds through the skin when backlit
• Compute the dot product between the
negative light vector and the view vector
• Multiply result by Fresnel term
• Only shows up if there is a light roughly
“behind” the object
Pixel Shader
float4 main (PsInput i) : COLOR
{
// Normalize interpolated vectors.
float3 vNormal = normalize (i.normal);
float3 vView = normalize (i.viewVec);
float NdotV = SiDot3Clamp (vNormal, vView);
float fresnel = (1.0f - NdotV);
// Compute Object Light 0
float3 lightColor = 2.0 *
float3 vLight = normalize
float NdotL = SiDot3Clamp
float VdotL = SiDot3Clamp
float3 diffuse = saturate
SiGetObjectAmbientLightColor(0);
(i.oaLightVec0);
(vNormal, vLight);
(-vLight, vView);
((fresnel*VdotL+NdotL)*lightColor);
// Compute Object Light 1 & 2 in the same way
// Output diffuse and alpha from bump map (blur size)
. . .
Added Rim Light Result
No Rim Light
+
=
Lighting + Rim Light
Just Rim Light
Spatially Varying Blur
• Used to simulate the subsurface
component of skin lighting
• Used a grow-able Poisson disc filter
• Read the kernel size from a texture
• Allows varying the subsurface effect
– Higher for places like ears/nose
– Lower for places like cheeks
Growable Filter Kernel
•
•
•
Stochastic sampling
Poisson distribution
Samples stored as 2D
offsets from center
Center Sample
Outer Samples
Small Blur
Large Blur
Spatially Varying Blur Pixel Shader
float4 main (PsInput i) : COLOR
{
float2 poisson[12] = . . . // Texel offsets from center
// Figure out blur size
float4 center = tex2D(tRenderedScenePong, i.texCoord);
float blurSize = center.a*vBlurScale.x + vBlurScale.y;
// Loop over the taps summing contributions
float3 cOut = center.rgb;
for (int tap = 0; tap < 12; tap++)
{
// Sample using Poisson taps
float2 coord = i.texCoord.xy+(vPixelSize*poisson[tap]*blurSize);
float4 sample = tex2D (tRenderedScenePong, coord);
cOut += sample.rgb;
}
return float4(cOut / 13.0f, center.a);
}
Blur Size Map and Blurred Lit Texture
Blur Kernel Size Map
Texture Space Lighting
Result
Dilation
• Texture seams can be a problem
(unused texels, bilinear blending artifacts)
• During the blur pass we need to dilate
• Use the alpha channel of off-screen
texture to determine where we wrote
• If any sample has 1.0 alpha, just copy the
sample with the lowest alpha
Dilation + Blur Pixel Shader Code
float4 main (PsInput i) : COLOR
{
float2 poisson[12] = // Texel offsets from center
// Figure out blur size
float4 center = tex2D(tRenderedScenePong, i.texCoord);
float blurSize = center.a*vBlurScale.x + vBlurScale.y;
// flag is the max alpha value. If it is 1.0f, then sample is
// close to the boundary since we clear alpha to 1.0
float flag = center.a;
Main Dilate/Blur Pixel Shader Loop
// Loop over the taps summing contributions
float3 cOut = center.rgb;
for (int tap = 0; tap < 12; tap++)
{
// Sample using Poisson distribution
float2 coord = i.texCoord.xy + (vPixelSize*poisson[tap]*blurSize);
float4 sample = tex2D (tRenderedScenePing, coord);
cOut += sample.rgb;
// Figure out if we need to change the flag
flag = max (sample.a, flag);
if (sample.a < center.a)
{
// Store texel with lowest alpha; will be used if close to
// the boundary to "dilate" by picking a more "inside" texel
center = sample;
}
}
Dilate Test Pixel Shader
// Test the flag to see if we are on a boundary texel
if (flag == 1.0f)
{
// On a boundary pick the texel with the lowest alpha
return float4 (center.rgb, 1.0f);
}
else
{
// Not on a boundary same blur as before.
return float4(cOut / 13.0f, 0.0f);
}
}
Dilation Results
Without Dilation
With Dilation
Shadows
• Used shadow maps
– Apply shadows during texture lighting
– Get “free” blur
•
•
•
•
Soft shadows
Simulates subsurface interaction
Lower precision/size requirements
Reduces artifacts
• Only doing shadows from one key light
Shadow Maps
• Create projection matrix to
generate map from the light’s point
of view
• Used bounding sphere of head to
ensure texture space is used
efficiently
• Write depth from light into offscreen texture
• Test depth values in pixel shader
Texture Lighting With Shadows
Write distance from light
into shadow map
Light in
Texture Space
Blur /
Dilate
Geometry
Sample texture space light
Back
Buffer
Shadow Map Vertex Shader
float4x4 mSiLightProjection; // Light projection matrix
VsOutput main (VsInput i)
{
VsOutput o;
// Compose skinning matrix
float4x4 mSkinning = SiComputeSkinningMatrix(i.weights, i.indices);
// Skin position/normal and multiply by light matrix
float4 pos = mul (i.pos, mSkinning);
o.pos = mul (pos, mSiLightProjection);
// Compute depth (Pixel Shader is just pass through)
float dv = o.pos.z/o.pos.w;
o.depth = float4(dv, dv, dv, 1);
return o;
}
Texture Lighting Vertex Shader
with Shadows
VsOutput main (VsInput i)
{
// Same lead in code as before
. . .
// Compute texture coordintates for shadow map
o.posLight = mul(pos, mSiLightKingPin);
o.posLight /= o.posLight.w;
o.posLight.xy = (o.posLight.xy + 1.0f)/2.0f;
o.posLight.y = 1.0f-o.posLight.y;
o.posLight.z -= 0.01f;
return o;
}
Texture Lighting Pixel Shader
with Shadows
sampler tShadowMap;
float faceShadowFactor;
float4 main (PsInput i) : COLOR
{
// Same lead in code
. . .
// Compute Object Light 0
float3 lightColor = 2.0 * SiGetObjectAmbientLightColor(0);
float3 vLight = normalize (i.oaLightVec0);
float NdotL = SiDot3Clamp (vNormal, vLight);
float VdotL = SiDot3Clamp (-vLight, vView);
float4 t = tex2D(tShadowMap, i.posLight.xy);
float lfac = faceShadowFactor;
if (i.posLight.z < t.z) lfac = 1.0f;
float3 diffuse = lfac * saturate ((fresnel*VdotL+NdotL)*lightColor);
. . .// The rest of the shader is the same as before
}
Shadow Map and
Shadowed Lit Texture
Shadow Map (depth)
Shadows in Texture Space
Result with Shadows
Shadows From Translucent Objects
• Allow multiple translucent objects that
combine to form opaque shadow (hair)
• Draw opaque shadow geometry first
• Blend alpha of translucent shadow geometry
into shadow buffer alpha. Don’t write depth!
• In pixel shader: non-shadowed pixels lerp
between shadow term and 1.0 based on
alpha in shadow map
Texture Lighting With
Translucent Shadows
Write distance into shadow
map for opaque geometry
Blend Alpha into
Shadow Map
Light in
Texture Space
Transparent
Geometry
Blur/
Dilate
Geometry
Sample texture space light
Back
Buffer
Translucent Shadow Pixel
Shader
float shadowAlpha;
float4 main (PsInput i) : COLOR
{
// Same lead in
. . .
// Usual light 0 code
. . .
float4 t = tex2D(tShadowMap, i.posLight.xy);
float lfac = faceShadowFactor;
if (i.posLight.z < t.z)
{
float alpha = pow(t.a, shadowAlpha);
lfac = lerp(faceShadowFactor, 1.0f, alpha);
}
float3 diffuse = lfac * saturate((fresnel*VdotL+NdotL)*lightColor);
. . . // Rest of the shader is the same as well
Shadow Map for Transparent
Shadows
Alpha
Shadow Map Fully Opaque
Shadow Map With
Translucency
Off Screen Light Textures
with Translucent Shadows
Opaque Shadows
Translucent Shadows
Translucent Shadows Results
Opaque Shadows
Translucent Shadows
Specular
• Use bump map for specular lighting
• Per-pixel exponent
• Need to shadow specular
– Hard to blur shadow map directly
– Expensive to do yet another blur pass for shadows
– Modulate specular from shadowing light by luminance of
texture space light
– Darkens specular in shadowed areas but preserves
lighting in unshadowed areas
• Shadow only dims one light (2 other un-shadowed)
Final Pixel Shader (with specular)
sampler tBase;
sampler tBump;
sampler tTextureLit;
float4 vBumpScale;
float specularDim;
float4 main (PsInput i)
{
// Get base and bump
float4 cBase = tex2D
float3 cBump = tex2D
: COLOR
map
(tBase, i.texCoord.xy);
(tBump, i.texCoord.xy);
// Get bumped normal
float3 vNormal = SiConvertColorToVector (cBump);
vNormal.z = vNormal.z * vBumpScale.x;
vNormal = normalize (vNormal);
Final Pixel Shader
// View, reflection, and specular exponent
float3 vView = normalize (i.viewVec);
float3 vReflect = SiReflect (vView, vNormal);
float exponent = cBase.a*vBumpScale.z + vBumpScale.w;
// Get "subsurface" light from lit texture.
float2 iTx = i.texCoord.xy;
iTx.y = 1-i.texCoord.y;
float4 cLight = tex2D (tTextureLit, iTx);
float3 diffuse = cLight*cBase;
Final Pixel Shader
// Compute Object Light 0
float3 lightColor = 2.0 * SiGetObjectAmbientLightColor(0);
float3 vLight = normalize (i.oaLightVec0);
float RdotL = SiDot3Clamp (vReflect, vLight);
float shadow = SiGetLuminance (cLight.rgb);
shadow = pow(shadow, 2);
float3 specular = saturate(pow(RdotL,exponent)*lightColor)*shadow;
// Compute Object Light 1 & 2 (same as above but no shadow term)
. . .
// Final color
float4 o;
o.rgb = diffuse + specular*specularDim;
o.a = 1.0;
return o;
}
Specular Shadow Dim Results
Specular Without Shadows
Specular With Shadows
Demo
Questions?