US3S8L6——菲涅尔反射

菲涅耳反射效果

菲涅耳反射是一种光学现象。简单的讲,就是视线垂直于观察表面时,反射较弱(更多的光会透射到进入新介质)
而当视线非垂直观察表面时,夹角越小,反射越明显。

真实世界中的例子:
如果你站在湖边,低头看脚下的水,你会发现水是透明的,反射不是特别强烈;
如果你看远处的湖面,你会发现水并不是透明的,但反射非常强烈,这就是“菲涅耳效应”。

imageimage

菲涅耳反射原理是描述光在两种介质的界面上反射和折射的行为。反射和折射的强度取决于光的入射角和两种介质的折射率差异。

从物理角度来看,世界上所有物体在光线照射下都会遵循菲涅耳反射原理!

这意味着光在任何两种介质的界面上都会发生反射和折射,只不过具体反射效果会因为介质的性质和光的入射角度不同而有所变化。
无论是透明的、半透明的还是不透明的物体,只要有光线入射到其表面,菲涅耳反射都会发生。

在 Unity Shader 中,菲涅耳反射一般用来增强真实感,使物体表面在不同角度和光照条件下呈现出更加真实和自然的外观,增强视觉效果。
因为刚才我们提到真实世界中,万物皆遵循菲涅耳反射原理,
那么我们在游戏中使用 菲涅耳反射 比 直接使用反射 能更加接近真实世界的效果,再实现一些金属感、陶瓷感、玻璃感时效果更好!

也就是说菲涅耳反射在 Unity Shader 当中实现出来的效果基于反射,但是会比反射更加接近真实的效果

菲涅耳反射效果的原理

菲涅耳反射的原理还是利用立方体纹理(CubeMap)进行环境映射,我们在计算反射光向量时,还是利用之前反射的一套规则去进行计算。
只是最终我们决定采样颜色时,需要为使用的反射率变量添加新的计算规则

1
2
3
4
5
Properties
{
_Cube("Cubemap", Cube) = ""{} // 立方体纹理
_Reflectivity("Reflectivity", Range(0, 1)) = 1 // 反射率,我们需要对反射率添加新的计算规则
}

imageimage

我们将使用菲涅耳等式来计算反射率,但是物理学中的菲涅耳等式是非常复杂的,如果在实时渲染中使用,计算非常复杂。

因此我们通常会使用近似公式来计算,在 Unity Shader 当中我们会使用 Schlick 菲涅耳近似公式来计算反射率
该公式是由 Christophe Schlick(克里斯托夫·施利克)于1994年提出的,
它提出的菲涅耳近似等式极大的简化了菲涅耳反射率的计算,非常适用于实时渲染,同时它还保留了足够的物理真实性!

具体公式如下:

R(θ)=R0+(1R0)(1cos(θ))5R(θ)=R0+(1R0)(1VN)5\begin{align*} R(\theta) & = R_0 + (1 - R_0)(1-cos(\theta))^5 \\ \Rightarrow R(\theta) & = R_0 + (1 - R_0)(1-V \cdot N)^5 \end{align*}

其中:

  • R(θ)R(\theta) 表示入射角为 θ\theta 时的反射率
  • R0R_0 是垂直入射某介质时的反射率
  • VV 是视角方向单位向量(入射角)
  • NN 是顶点法线单位向量

image

该公式中视角方向 VV 和顶点法线 NN 都是已知的,我们只需要查阅对应物体在真实世界中的反射率 R0R_0
将该反射率带入公式进行计算,就可以得到菲涅耳反射率,让其参与最终的颜色计算即可!

注意!通常情况下,我们所说的物体的反射率(或反射系数)通常是指垂直入射光线时的反射率

菲涅尔反射的基础实现

  1. 新建Shader,复用基础反射Shader中代码,在其基础上进行修改

    具体内容详见:US3S8L4——反射效果 的基础反射实现部分

  2. 属性声明将反射率变量修改为 _FresnelScale​ 菲涅耳中介质的反射率

    1
    2
    3
    4
    5
    Properties
    {
    _Cube("Cubemap", Cube) = ""{} // 立方体纹理
    _FresnelScale("_FresnelScale", Range(0, 1)) = 1 // 菲涅尔反射中,对应介质的反射率
    }
  3. 结构体

    由于使用 ​Schlick 菲涅耳近似等式,需要用到世界空间下视角方向、和法线向量,因此在结构体中加入这两个变量

    1
    2
    3
    4
    5
    6
    7
    struct v2f
    {
    float4 pos: SV_POSITION; // 裁剪空间下的顶点坐标
    float3 worldNormal: NORMAL; // 世界空间下的法线
    float3 worldViewDir: TEXCOORD0; // 世界空间下的视角方向
    float3 worldReflection: TEXCOORD1; // 世界空间下的反射向量
    };
  4. 顶点着色器

    关键步骤:添加结构体中变量赋值

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    v2f vert(appdata_base v)
    {
    v2f data;
    data.pos = UnityObjectToClipPos(v.vertex); // 顶点坐标转裁剪坐标
    data.worldNormal = UnityObjectToWorldNormal(v.normal); // 顶点法线转世界坐标
    fixed3 worldPos = mul(unity_ObjectToWorld, v.vertex).xyz; // 顶点坐标装世界坐标
    data.worldViewDir = UnityWorldSpaceViewDir(worldPos); // 计算世界空间下视角所在方向
    data.worldReflection = reflect(-data.worldViewDir, data.worldNormal); // 通过反向视角方向得到反射方向

    return data;
    }
  5. 片元着色器

    关键步骤:利用 Schlick 菲涅耳近似等式计算出菲涅耳反射率,参与最终颜色计算

    1
    2
    3
    4
    5
    6
    7
    8
    9
    fixed4 frag(v2f i): SV_TARGET
    {
    // 从立方体纹理内采样得到反射颜色
    fixed4 cubemapColor = texCUBE(_Cube, i.worldReflection);
    // 根据schlick菲涅尔近似公式,计算菲涅尔反射率
    fixed fresnal = _FresnelScale + (1 - _FresnelScale) * pow((1 - dot(normalize(i.worldViewDir), normalize(i.worldNormal))), 5);
    // 计算最终颜色
    return cubemapColor * fresnal;
    }

显示效果(左为纯反射Shader,反射率为0.2,右为菲涅尔反射Shader,菲涅尔介质反射率为0.2):

image

可见,和左边的全黑相比,右边的菲涅尔反射在茶壶的边缘处呈现了更亮的反射效果

完整 Shader 代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
Shader "TeachShader/Lesson79_FresnelBase"
{
Properties
{
_Cube("Cubemap", Cube) = ""{} // 立方体纹理
_FresnelScale("_FresnelScale", Range(0, 1)) = 1 // 菲涅尔反射中,对应介质的反射率
}
SubShader
{
Tags { "RenderType" = "Opaque" "Queue" = "Geometry" }

Pass
{
Tags { "LightMode" = "ForwardBase" }

CGPROGRAM
#pragma vertex vert
#pragma fragment frag

#include "UnityCG.cginc"
#include "Lighting.cginc"

samplerCUBE _Cube;
float _FresnelScale;

struct v2f
{
float4 pos: SV_POSITION; // 裁剪空间下的顶点坐标
float3 worldNormal: NORMAL; // 世界空间下的法线
float3 worldViewDir: TEXCOORD0; // 世界空间下的视角方向
float3 worldReflection: TEXCOORD1; // 世界空间下的反射向量
};

v2f vert(appdata_base v)
{
v2f data;
data.pos = UnityObjectToClipPos(v.vertex); // 顶点坐标转裁剪坐标
data.worldNormal = UnityObjectToWorldNormal(v.normal); // 顶点法线转世界坐标
fixed3 worldPos = mul(unity_ObjectToWorld, v.vertex).xyz; // 顶点坐标装世界坐标
data.worldViewDir = UnityWorldSpaceViewDir(worldPos); // 计算世界空间下视角所在方向
data.worldReflection = reflect(-data.worldViewDir, data.worldNormal); // 通过反向视角方向得到反射方向

return data;
}

fixed4 frag(v2f i): SV_TARGET
{
// 从立方体纹理内采样得到反射颜色
fixed4 cubemapColor = texCUBE(_Cube, i.worldReflection);
// 根据schlick菲涅尔近似公式,计算菲涅尔反射率
fixed fresnal = _FresnelScale + (1 - _FresnelScale) * pow((1 - dot(normalize(i.worldViewDir), normalize(i.worldNormal))), 5);
// 计算最终颜色
return cubemapColor * fresnal;
}
ENDCG
}
}
}

菲涅尔反射结合漫反射和阴影

由于菲涅耳反射是基于反射的,因此我们在之前实现的反射结合漫反射的基础上修改即可

  1. 新建 Shader,复用反射结合漫反射和阴影 Shader 中的代码,在其基础上进行修改

    具体内容详见:US3S8L4——反射效果 的 反射结合漫反射和阴影 实现部分

  2. 修改属性

    1. 去掉反射颜色属性
    2. 修改反射率属性为菲涅耳相关的 R0R_0 反射率属性
    1
    2
    3
    4
    5
    6
    Properties
    {
    _Color("Color", Color) = (1, 1, 1, 1) // 漫反射颜色
    _Cube("Cubemap", Cube) = ""{} // 立方体纹理
    _FresnelScale("_FresnelScale", Range(0, 1)) = 1 // 菲涅尔反射中对应介质的反射率
    }
  3. 修改 v2f​ 结构体

    加入视角方向(需要在片元着色器中用菲涅耳近似公式参与计算)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    struct v2f
    {
    float4 pos: SV_POSITION; // 裁剪空间下的顶点坐标
    fixed3 worldNormal: NORMAL; // 世界空间下法线
    float3 worldPos: TEXCOORD0; // 世界空间下顶点坐标
    float3 worldViewDir: TEXCOORD1; // 世界空间下的视角方向
    float3 worldReflection: TEXCOORD2; // 世界空间下的反射向量
    SHADOW_COORDS(3) // 阴影坐标宏
    };
  4. 顶点着色器

    修改视角方向临时变量

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    v2f vert(appdata_base v)
    {
    v2f data;
    data.pos = UnityObjectToClipPos(v.vertex); // 顶点坐标转裁剪坐标
    data.worldNormal = UnityObjectToWorldNormal(v.normal); // 顶点法线转世界坐标
    data.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz; // 顶点坐标装世界坐标
    data.worldViewDir = UnityWorldSpaceViewDir(data.worldPos); // 计算世界空间下视角所在方向
    data.worldReflection = reflect(-data.worldViewDir, data.worldNormal); // 通过反向视角方向得到反射方向
    TRANSFER_SHADOW(data) // 阴影转换计算

    return data;
    }
  5. 片元着色器

    1. 计算菲涅耳反射率
    2. 用菲涅耳反射率参与 lerp​ 计算
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    fixed4 frag(v2f i): SV_TARGET
    {
    // 漫反射计算
    fixed3 worldLightDir = normalize(UnityWorldSpaceLightDir(i.worldPos));
    fixed3 diffuse = _LightColor0.rgb * _Color.rgb * max(0, dot(normalize(i.worldNormal), worldLightDir));
    // 立方体纹理采样反射颜色
    fixed3 cubemapColor = texCUBE(_Cube, i.worldReflection).rgb;
    // 计算光照衰减和阴影相关的衰减值
    UNITY_LIGHT_ATTENUATION(atten, i, i.worldPos)
    // 根据schlick菲涅尔近似公式,计算菲涅尔反射率
    fixed fresnal = _FresnelScale + (1 - _FresnelScale) * pow((1 - dot(normalize(i.worldViewDir), normalize(i.worldNormal))), 5);
    // 利用插值计算,在 漫反射颜色 和 反射颜色 之间进行插值,0和1就是极限状态,0代表没有反射效果,1代表只要反射效果,0~1就是两者的叠加
    fixed3 color = UNITY_LIGHTMODEL_AMBIENT.rgb + lerp(diffuse, cubemapColor, fresnal) * atten;
    return fixed4(color, 1);
    }

材质预览效果(左为使用了菲涅尔反射的Shader的材质球,右为不使用菲涅尔反射的材质球):

image

显示效果(左不使用菲涅尔反射的Shader,反射率为0.05,右使用菲涅尔反射的Shader,菲涅尔介质反射率为0.05):

image

可以看到,使用菲涅尔反射的Shader,即使在反射率较低的情况下,在边缘处依然有明显的反射效果,越接近边缘,反射效果越明显

完整 Shader 如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
Shader "TeachShader/Lesson80_Fresnel"
{
Properties
{
_Color("Color", Color) = (1, 1, 1, 1) // 漫反射颜色
_Cube("Cubemap", Cube) = ""{} // 立方体纹理
_FresnelScale("_FresnelScale", Range(0, 1)) = 1 // 菲涅尔反射中对应介质的反射率
}
SubShader
{
Tags { "RenderType" = "Opaque" "Queue" = "Geometry" }

Pass
{
Tags { "LightMode" = "ForwardBase" }

CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#pragma multi_compile_fwdbase

#include "UnityCG.cginc"
#include "Lighting.cginc"
#include "AutoLight.cginc"

fixed4 _Color;
samplerCUBE _Cube;
float _FresnelScale;

struct v2f
{
float4 pos: SV_POSITION; // 裁剪空间下的顶点坐标
fixed3 worldNormal: NORMAL; // 世界空间下法线
float3 worldPos: TEXCOORD0; // 世界空间下顶点坐标
float3 worldViewDir: TEXCOORD1; // 世界空间下的视角方向
float3 worldReflection: TEXCOORD2; // 世界空间下的反射向量
SHADOW_COORDS(3) // 阴影坐标宏
};

v2f vert(appdata_base v)
{
v2f data;
data.pos = UnityObjectToClipPos(v.vertex); // 顶点坐标转裁剪坐标
data.worldNormal = UnityObjectToWorldNormal(v.normal); // 顶点法线转世界坐标
data.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz; // 顶点坐标装世界坐标
data.worldViewDir = UnityWorldSpaceViewDir(data.worldPos); // 计算世界空间下视角所在方向
data.worldReflection = reflect(-data.worldViewDir, data.worldNormal); // 通过反向视角方向得到反射方向
TRANSFER_SHADOW(data) // 阴影转换计算

return data;
}

fixed4 frag(v2f i): SV_TARGET
{
// 漫反射计算
fixed3 worldLightDir = normalize(UnityWorldSpaceLightDir(i.worldPos));
fixed3 diffuse = _LightColor0.rgb * _Color.rgb * max(0, dot(normalize(i.worldNormal), worldLightDir));
// 立方体纹理采样反射颜色
fixed3 cubemapColor = texCUBE(_Cube, i.worldReflection).rgb;
// 计算光照衰减和阴影相关的衰减值
UNITY_LIGHT_ATTENUATION(atten, i, i.worldPos)
// 根据schlick菲涅尔近似公式,计算菲涅尔反射率
fixed fresnal = _FresnelScale + (1 - _FresnelScale) * pow((1 - dot(normalize(i.worldViewDir), normalize(i.worldNormal))), 5);
// 利用插值计算,在 漫反射颜色 和 反射颜色 之间进行插值,0和1就是极限状态,0代表没有反射效果,1代表只要反射效果,0~1就是两者的叠加
fixed3 color = UNITY_LIGHTMODEL_AMBIENT.rgb + lerp(diffuse, cubemapColor, fresnal) * atten;
return fixed4(color, 1);
}
ENDCG
}
}

Fallback "Reflective/VertexLit"
}