GLSL 程式設計/Unity/輪廓處的鏡面高光

本教程涵蓋了鏡面高光的菲涅爾係數。
它是關於光照的多個教程之一,這些教程超越了 Phong 反射模型。但是,它基於使用 Phong 反射模型進行的光照,如“鏡面高光”部分(針對每個頂點的光照)和“平滑鏡面高光”部分(針對每個畫素的光照)中所述。如果您還沒有閱讀這些教程,建議您先閱讀。
許多材料(如啞光紙)在光線掠過表面時會顯示出強烈的鏡面反射;即,當背光從與觀察者相反的方向反射時,如左側的照片所示。菲涅爾係數解釋了一些材料的這種強反射。當然,輪廓明亮還有其他原因,例如半透明的頭髮或織物(見“半透明表面”部分)。
有趣的是,這種效果通常很難察覺,因為它最有可能發生在輪廓背景非常明亮的情況下。但是,在這種情況下,明亮的輪廓將與背景融合,因此幾乎無法察覺。

菲涅爾係數 描述了非導電材料對波長為 的非偏振光的鏡面反射率。Schlick 近似為
其中 V 是歸一化的觀察者方向,H 是歸一化的半程向量:H = (V + L) / |V + L|,其中 L 是歸一化的光源方向。 是 H·V = 1 時的反射率,即當光源方向、觀察者方向和半程向量都相同時。另一方面, 在 H·V = 0 時變為 1,即當半程向量與觀察者方向正交時,這意味著光源方向與觀察者方向相反(即掠入射光反射的情況)。事實上, 在這種情況下與波長無關,材料的行為就像一個完美的鏡子。
使用內建的 GLSL 函式 mix(x,y,w) = x*(1-w) + y*w,我們可以將 Schlick 近似改寫為
這可能稍微更有效,至少在某些 GPU 上是如此。我們將透過允許不同的 值來考慮對波長的依賴性,每個顏色分量;也就是說,我們將它視為一個 RGB 向量。事實上,我們將它與來自 “鏡面高光”部分 的常數材質顏色 相一致。換句話說,菲涅爾因子增加了材質顏色 對觀察者方向和半程向量之間角度的依賴性。因此,我們在任何鏡面反射計算中用施裡克近似值(使用 )替換了常數材質顏色 。
例如,我們在 Phong 反射模型中對鏡面項的方程式是(參見 “鏡面高光”部分)
用 Schlick 近似值來替換菲涅爾因子中的 ,其中 ,得到
該實現基於來自“平滑鏡面高光”部分的著色器程式碼。它只是計算了半程向量,幷包含了菲涅耳因子的近似值
vec3 specularReflection;
if (dot(normalDirection, lightDirection) < 0.0)
// light source on the wrong side?
{
specularReflection = vec3(0.0, 0.0, 0.0);
// no specular reflection
}
else // light source on the right side
{
vec3 halfwayDirection =
normalize(lightDirection + viewDirection);
float w = pow(1.0 - max(0.0,
dot(halfwayDirection, viewDirection)), 5.0);
specularReflection = attenuation * vec3(_LightColor0)
* mix(vec3(_SpecColor), vec3(1.0), w)
* pow(max(0.0, dot(
reflect(-lightDirection, normalDirection),
viewDirection)), _Shininess);
}
將上面的程式碼片段放入來自“平滑鏡面高光”部分的完整著色器中,會得到這個著色器
Shader "GLSL 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
GLSLPROGRAM
// User-specified properties
uniform vec4 _Color;
uniform vec4 _SpecColor;
uniform float _Shininess;
// The following built-in uniforms (except _LightColor0)
// are also defined in "UnityCG.glslinc",
// i.e. one could #include "UnityCG.glslinc"
uniform vec3 _WorldSpaceCameraPos;
// camera position in world space
uniform mat4 _Object2World; // model matrix
uniform mat4 _World2Object; // inverse model matrix
uniform vec4 _WorldSpaceLightPos0;
// direction to or position of light source
uniform vec4 _LightColor0;
// color of light source (from "Lighting.cginc")
varying vec4 position;
// position of the vertex (and fragment) in world space
varying vec3 varyingNormalDirection;
// surface normal vector in world space
#ifdef VERTEX
void main()
{
mat4 modelMatrix = _Object2World;
mat4 modelMatrixInverse = _World2Object; // unity_Scale.w
// is unnecessary because we normalize vectors
position = modelMatrix * gl_Vertex;
varyingNormalDirection = normalize(vec3(
vec4(gl_Normal, 0.0) * modelMatrixInverse));
gl_Position = gl_ModelViewProjectionMatrix * gl_Vertex;
}
#endif
#ifdef FRAGMENT
void main()
{
vec3 normalDirection = normalize(varyingNormalDirection);
vec3 viewDirection =
normalize(_WorldSpaceCameraPos - vec3(position));
vec3 lightDirection;
float attenuation;
if (0.0 == _WorldSpaceLightPos0.w) // directional light?
{
attenuation = 1.0; // no attenuation
lightDirection = normalize(vec3(_WorldSpaceLightPos0));
}
else // point or spot light
{
vec3 vertexToLightSource =
vec3(_WorldSpaceLightPos0 - position);
float distance = length(vertexToLightSource);
attenuation = 1.0 / distance; // linear attenuation
lightDirection = normalize(vertexToLightSource);
}
vec3 ambientLighting =
vec3(gl_LightModel.ambient) * vec3(_Color);
vec3 diffuseReflection =
attenuation * vec3(_LightColor0) * vec3(_Color)
* max(0.0, dot(normalDirection, lightDirection));
vec3 specularReflection;
if (dot(normalDirection, lightDirection) < 0.0)
// light source on the wrong side?
{
specularReflection = vec3(0.0, 0.0, 0.0);
// no specular reflection
}
else // light source on the right side
{
vec3 halfwayDirection =
normalize(lightDirection + viewDirection);
float w = pow(1.0 - max(0.0,
dot(halfwayDirection, viewDirection)), 5.0);
specularReflection = attenuation * vec3(_LightColor0)
* mix(vec3(_SpecColor), vec3(1.0), w)
* pow(max(0.0, dot(
reflect(-lightDirection, normalDirection),
viewDirection)), _Shininess);
}
gl_FragColor = vec4(ambientLighting
+ diffuseReflection + specularReflection, 1.0);
}
#endif
ENDGLSL
}
Pass {
Tags { "LightMode" = "ForwardAdd" }
// pass for additional light sources
Blend One One // additive blending
GLSLPROGRAM
// User-specified properties
uniform vec4 _Color;
uniform vec4 _SpecColor;
uniform float _Shininess;
// The following built-in uniforms (except _LightColor0)
// are also defined in "UnityCG.glslinc",
// i.e. one could #include "UnityCG.glslinc"
uniform vec3 _WorldSpaceCameraPos;
// camera position in world space
uniform mat4 _Object2World; // model matrix
uniform mat4 _World2Object; // inverse model matrix
uniform vec4 _WorldSpaceLightPos0;
// direction to or position of light source
uniform vec4 _LightColor0;
// color of light source (from "Lighting.cginc")
varying vec4 position;
// position of the vertex (and fragment) in world space
varying vec3 varyingNormalDirection;
// surface normal vector in world space
#ifdef VERTEX
void main()
{
mat4 modelMatrix = _Object2World;
mat4 modelMatrixInverse = _World2Object; // unity_Scale.w
// is unnecessary because we normalize vectors
position = modelMatrix * gl_Vertex;
varyingNormalDirection = normalize(vec3(
vec4(gl_Normal, 0.0) * modelMatrixInverse));
gl_Position = gl_ModelViewProjectionMatrix * gl_Vertex;
}
#endif
#ifdef FRAGMENT
void main()
{
vec3 normalDirection = normalize(varyingNormalDirection);
vec3 viewDirection =
normalize(_WorldSpaceCameraPos - vec3(position));
vec3 lightDirection;
float attenuation;
if (0.0 == _WorldSpaceLightPos0.w) // directional light?
{
attenuation = 1.0; // no attenuation
lightDirection = normalize(vec3(_WorldSpaceLightPos0));
}
else // point or spot light
{
vec3 vertexToLightSource =
vec3(_WorldSpaceLightPos0 - position);
float distance = length(vertexToLightSource);
attenuation = 1.0 / distance; // linear attenuation
lightDirection = normalize(vertexToLightSource);
}
vec3 diffuseReflection =
attenuation * vec3(_LightColor0) * vec3(_Color)
* max(0.0, dot(normalDirection, lightDirection));
vec3 specularReflection;
if (dot(normalDirection, lightDirection) < 0.0)
// light source on the wrong side?
{
specularReflection = vec3(0.0, 0.0, 0.0);
// no specular reflection
}
else // light source on the right side
{
vec3 halfwayDirection =
normalize(lightDirection + viewDirection);
float w = pow(1.0 - max(0.0,
dot(halfwayDirection, viewDirection)), 5.0);
specularReflection = attenuation * vec3(_LightColor0)
* mix(vec3(_SpecColor), vec3(1.0), w)
* pow(max(0.0, dot(
reflect(-lightDirection, normalDirection),
viewDirection)), _Shininess);
}
gl_FragColor =
vec4(diffuseReflection + specularReflection, 1.0);
}
#endif
ENDGLSL
}
}
// The definition of a fallback shader should be commented out
// during development:
// Fallback "Specular"
}
上面實現的一個有用修改是將冪5.0替換為使用者指定的著色器屬性。這將使 CG 藝術家根據他們的藝術需求來誇大或衰減菲涅耳因子的影響。
除了影響鏡面高光之外,菲涅耳因子還應該影響半透明表面的不透明度。事實上,菲涅耳因子描述了表面如何對掠射光線變得更具反射性,這意味著吸收、折射或透射的光線更少,即透明度降低,因此不透明度提高。為此,可以使用表面法線向量N而不是半程向量H來計算菲涅耳因子,半透明表面的不透明度可以從使用者指定的數值(在表面法線方向的觀察中)增加到 1(獨立於波長)與
.
在“輪廓增強”部分,不透明度被認為是由光線穿過一層半透明材料時衰減引起的。這種不透明度應該與由於反射率增加而導致的不透明度結合起來:總的不透明度是 1 減去總透明度,它是由於衰減引起的透明度(即 1 減去)和由於菲涅耳因子引起的透明度(即 1 減去)的乘積,即
是不透明度,如上計算,而是不透明度,如“輪廓增強”部分中計算的那樣。對於平行於表面法向量的視角,和可以由使用者指定。然後,該方程固定用於法線方向,實際上,它固定了所有常數,因此可以針對所有視角進行計算。請注意,漫反射或鏡面反射都不應與不透明度相乘,因為鏡面反射已經與菲涅耳因子相乘,而漫反射應該只與由於衰減引起的不透明度相乘。
恭喜你完成了其中一個比較高階的教程!我們已經瞭解了:
- 什麼是菲涅爾係數。
- 施裡克對菲涅爾係數的近似方法。
- 如何在鏡面高光中實現施裡克的近似方法。
- 如何為實現新增更多藝術控制。
- 如何將菲涅爾係數用於半透明表面。
如果你還想了解更多
- 關於使用 Phong 反射模型進行光照,你可以閱讀 “鏡面高光”部分。
- 關於逐畫素光照(即 Phong 著色),你可以閱讀 “平滑鏡面高光”部分。
- 關於施裡克的近似方法,你可以閱讀他的文章“一種用於物理基礎渲染的廉價 BRDF 模型”,作者 Christophe Schlick,計算機圖形論壇,13(3):233—246,1994 年。或者你可以閱讀書籍“OpenGL 著色語言”(第 3 版)第 14.1 章,作者 Randi Rost 等,2009 年由 Addison-Wesley 出版,或書籍“程式設計頂點、幾何體和畫素著色器”(第 2 版,2008 年)的“光照”章節中的第 5 節,作者 Wolfgang Engel、Jack Hoxley、Ralf Kornmann、Niko Suni 和 Jason Zink(可以在 網上 獲得)。