Cg Programming/Unity/Specular Highlights at Silhouettes
This tutorial covers the Fresnel factor for specular highlights.
It is one of several tutorials about lighting that go beyond the Phong reflection model. However, it is based on lighting with the Phong reflection model as described in Section “Specular Highlights” (for per-vertex lighting) and Section “Smooth Specular Highlights” (for per-pixel lighting). If you haven't read those tutorials yet, you should read them first.
Many materials (e.g. matte paper) show strong specular reflection when light grazes the surface; i.e., when backlight is reflected from the opposite direction to the viewer as in the photo to the left. The Fresnel factor explains this strong reflection for some materials. Of course, there are also other reasons for bright silhouettes, e.g. translucent hair or fabrics (see Section “Translucent Surfaces”).
Interestingly, the effect is often hardly visible because it is most likely when the background of the silhouette is very bright. In this case, however, a bright silhouette will just blend into the background and thus become hardly noticeable.
Schlick's Approximation of the Fresnel Factor
editThe Fresnel factor describes the specular reflectance of nonconducting materials for unpolarized light of wavelength . Schlick's approximation is:
where V is the normalized direction to the viewer and H is the normalized halfway vector: H = (V + L) / |V + L| with L being the normalized direction to the light source. is the reflectance for H·V = 1, i.e. when the direction to the light source, the direction to the viewer, and the halfway vector are all identical. On the other hand, becomes 1 for H·V = 0, i.e. when the halfway vector is orthogonal to the direction to the viewer, which means that the direction to the light source is opposite to the direction to the viewer (i.e. the case of a grazing light reflection). In fact, is independent of the wavelength in this case and the material behaves just like a perfect mirror.
Using the built-in Cg function lerp(x,y,w) = x*(1-w) + y*w
we can rewrite Schlick's approximation as:
which might be slightly more efficient, at least on some GPUs. We will take the dependency on the wavelength into account by allowing for different values of for each color component; i.e. we consider it an RGB vector. In fact, we identify it with the constant material color from Section “Specular Highlights”. In other words, the Fresnel factor adds a dependency of the material color on the angle between the direction to the viewer and the halfway vector. Thus, we replace the constant material color with Schlick's approximation (using ) in any calculation of the specular reflection.
For example, our equation for the specular term in the Phong reflection model was (see Section “Specular Highlights”):
Replacing by Schlick's approximation for the Fresnel factor with yields:
Implementation
editThe implementation is based on the shader code from Section “Smooth Specular Highlights”. It just computes the halfway vector and includes the approximation of the Fresnel factor:
float3 specularReflection;
if (dot(normalDirection, lightDirection) < 0.0)
// light source on the wrong side?
{
specularReflection = float3(0.0, 0.0, 0.0);
// no specular reflection
}
else // light source on the right side
{
float3 halfwayDirection =
normalize(lightDirection + viewDirection);
float w = pow(1.0 - max(0.0,
dot(halfwayDirection, viewDirection)), 5.0);
specularReflection = attenuation * _LightColor0.rgb
* lerp(_SpecColor.rgb, float3(1.0, 1.0, 1.0), w)
* pow(max(0.0, dot(
reflect(-lightDirection, normalDirection),
viewDirection)), _Shininess);
}
Complete Shader Code
editPutting the code snippet from above in the complete shader from Section “Smooth Specular Highlights” results in this shader:
Shader "Cg Fresnel highlights" {
Properties {
_Color ("Diffuse Material Color", Color) = (1,1,1,1)
_SpecColor ("Specular Material Color", Color) = (1,1,1,1)
_Shininess ("Shininess", Float) = 10
}
SubShader {
Pass {
Tags { "LightMode" = "ForwardBase" }
// pass for ambient light and first light source
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
uniform float4 _LightColor0;
// color of light source (from "Lighting.cginc")
// User-specified properties
uniform float4 _Color;
uniform float4 _SpecColor;
uniform float _Shininess;
struct vertexInput {
float4 vertex : POSITION;
float3 normal : NORMAL;
};
struct vertexOutput {
float4 pos : SV_POSITION;
float4 posWorld : TEXCOORD0;
float3 normalDir : TEXCOORD1;
};
vertexOutput vert(vertexInput input)
{
vertexOutput output;
float4x4 modelMatrix = unity_ObjectToWorld;
float3x3 modelMatrixInverse = unity_WorldToObject;
output.posWorld = mul(modelMatrix, input.vertex);
output.normalDir = normalize(mul(input.normal, modelMatrixInverse));
output.pos = mul(UNITY_MATRIX_MVP, input.vertex);
return output;
}
float4 frag(vertexOutput input) : COLOR
{
float3 normalDirection = normalize(input.normalDir);
float3 viewDirection = normalize(
_WorldSpaceCameraPos - input.posWorld.xyz);
float3 lightDirection;
float attenuation;
if (0.0 == _WorldSpaceLightPos0.w) // directional light?
{
attenuation = 1.0; // no attenuation
lightDirection = normalize(_WorldSpaceLightPos0.xyz);
}
else // point or spot light
{
float3 vertexToLightSource =
_WorldSpaceLightPos0.xyz - input.posWorld.xyz;
float distance = length(vertexToLightSource);
attenuation = 1.0 / distance; // linear attenuation
lightDirection = normalize(vertexToLightSource);
}
float3 ambientLighting =
UNITY_LIGHTMODEL_AMBIENT.rgb * _Color.rgb;
float3 diffuseReflection =
attenuation * _LightColor0.rgb * _Color.rgb
* max(0.0, dot(normalDirection, lightDirection));
float3 specularReflection;
if (dot(normalDirection, lightDirection) < 0.0)
// light source on the wrong side?
{
specularReflection = float3(0.0, 0.0, 0.0);
// no specular reflection
}
else // light source on the right side
{
float3 halfwayDirection =
normalize(lightDirection + viewDirection);
float w = pow(1.0 - max(0.0,
dot(halfwayDirection, viewDirection)), 5.0);
specularReflection = attenuation * _LightColor0.rgb
* lerp(_SpecColor.rgb, float3(1.0, 1.0, 1.0), w)
* pow(max(0.0, dot(
reflect(-lightDirection, normalDirection),
viewDirection)), _Shininess);
}
return float4(ambientLighting
+ diffuseReflection + specularReflection, 1.0);
}
ENDCG
}
Pass {
Tags { "LightMode" = "ForwardAdd" }
// pass for additional light sources
Blend One One // additive blending
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
uniform float4 _LightColor0;
// color of light source (from "Lighting.cginc")
// User-specified properties
uniform float4 _Color;
uniform float4 _SpecColor;
uniform float _Shininess;
struct vertexInput {
float4 vertex : POSITION;
float3 normal : NORMAL;
};
struct vertexOutput {
float4 pos : SV_POSITION;
float4 posWorld : TEXCOORD0;
float3 normalDir : TEXCOORD1;
};
vertexOutput vert(vertexInput input)
{
vertexOutput output;
float4x4 modelMatrix = unity_ObjectToWorld;
float4x4 modelMatrixInverse = unity_WorldToObject;
output.posWorld = mul(modelMatrix, input.vertex);
output.normalDir = normalize(
mul(float4(input.normal, 0.0), modelMatrixInverse).xyz);
output.pos = mul(UNITY_MATRIX_MVP, input.vertex);
return output;
}
float4 frag(vertexOutput input) : COLOR
{
float3 normalDirection = normalize(input.normalDir);
float3 viewDirection = normalize(
_WorldSpaceCameraPos - input.posWorld.xyz);
float3 lightDirection;
float attenuation;
if (0.0 == _WorldSpaceLightPos0.w) // directional light?
{
attenuation = 1.0; // no attenuation
lightDirection = normalize(_WorldSpaceLightPos0.xyz);
}
else // point or spot light
{
float3 vertexToLightSource =
_WorldSpaceLightPos0.xyz - input.posWorld.xyz;
float distance = length(vertexToLightSource);
attenuation = 1.0 / distance; // linear attenuation
lightDirection = normalize(vertexToLightSource);
}
float3 diffuseReflection =
attenuation * _LightColor0.rgb * _Color.rgb
* max(0.0, dot(normalDirection, lightDirection));
float3 specularReflection;
if (dot(normalDirection, lightDirection) < 0.0)
// light source on the wrong side?
{
specularReflection = float3(0.0, 0.0, 0.0);
// no specular reflection
}
else // light source on the right side
{
float3 halfwayDirection =
normalize(lightDirection + viewDirection);
float w = pow(1.0 - max(0.0,
dot(halfwayDirection, viewDirection)), 5.0);
specularReflection = attenuation * _LightColor0.rgb
* lerp(_SpecColor.rgb, float3(1.0, 1.0, 1.0), w)
* pow(max(0.0, dot(
reflect(-lightDirection, normalDirection),
viewDirection)), _Shininess);
}
return float4(diffuseReflection
+ specularReflection, 1.0);
}
ENDCG
}
}
Fallback "Specular"
}
Artistic Control
editA useful modification of the implementation above is to replace the power 5.0
by a user-specified shader property. This would give CG artists the option to exaggerate or attenuate the effect of the Fresnel factor depending on their artistic needs.
Consequences for Semitransparent Surfaces
editApart from influencing specular highlights, a Fresnel factor should also influence the opacity of semitransparent surfaces. In fact, the Fresnel factor describes how a surface becomes more reflective for grazing light rays, which implies that less light is absorbed, refracted, or transmitted, i.e. the transparency decreases and therefore the opacity increases. To this end, a Fresnel factor could be computed with the surface normal vector N instead of the halfway vector H and the opacity of a semitransparent surface could increase from a user-specified value (for viewing in the direction of the surface normal) to 1 (independently of the wavelength) with
.
In Section “Silhouette Enhancement” the opacity was considered to result from an attenuation of light as it passes through a layer of semitransparent material. This opacity should be combined with the opacity due to increased reflectivity in the following way: the total opacity is 1 minus the total transparency which is the product of the transparency due to attenuation (which is 1 minus ) and the transparency due to the Fresnel factor (which is 1 minus ), i.e.:
is the opacity as computed above while is the opacity as computed in Section “Silhouette Enhancement”. For the view direction parallel to the surface normal vector, and could be specified by the user. Then the equation fixes for the normal direction and, in fact, it fixes all constants and therefore can be computed for all view directions. Note that neither the diffuse reflection nor the specular reflection should be multiplied with the opacity since the specular reflection is already multiplied with the Fresnel factor and the diffuse reflection should only be multiplied with the opacity due to attenuation .
Summary
editCongratulations, you finished one of the somewhat advanced tutorials! We have seen:
- What the Fresnel factor is.
- What Schlick's approximation to the Fresnel factor is.
- How to implement Schlick's approximation for specular highlights.
- How to add more artistic control to the implementation.
- How to use the Fresnel factor for semitransparent surfaces.
Further reading
editIf you still want to know more
- about lighting with the Phong reflection model, you should read Section “Specular Highlights”.
- about per-pixel lighting (i.e. Phong shading), you should read Section “Smooth Specular Highlights”.
- about Schlick's approximation, you should read his article “An inexpensive BRDF model for physically-based rendering” by Christophe Schlick, Computer Graphics Forum, 13(3):233—246, 1994. or you could read Section 14.1 of the book “OpenGL Shading Language” (3rd edition) by Randi Rost and others, published 2009 by Addison-Wesley, or Section 5 in the Lighting chapter of the book “Programming Vertex, Geometry, and Pixel Shaders” (2nd edition, 2008) by Wolfgang Engel, Jack Hoxley, Ralf Kornmann, Niko Suni, and Jason Zink (which is available online.)