US3S8L5——折射效果

本章代码关键字

1
refract()    // 折射向量计算

折射效果

折射效果指光的折射,是一种光学现象。指光从一种透明介质斜射入另一种透明介质时,传播方向一般会发生变化,这种现象叫光的折射。

光的折射与光的反射一样,都是发生在两种介质的交界处,只是反射光返回原介质中,而折射光进入到另一种介质中,
由于光在两种不同的物质里传播速度不同,故在两种介质的交界处传播方向发生变化,这就是光的折射。

imageimage

在 Unity Shader 中,折射效果模拟了光线通过透明或半透明材质时的弯曲行为。
一般用来模拟水面、透明玻璃球、眼镜、钻石、水晶球、空气扰动等等效果,它一般会配合其他表现效果一起使用

imageimage

折射效果的原理

折射效果的原理还是利用立方体纹理(CubeMap​)进行环境映射
我们利用摄像机看向物体表面顶点的方向向量作为入射向量,结合顶点法线向量算出折射向量
然后利用折射方向向量在立方体纹理中进行采样,得到最终反射的颜色。

简单来说,就是根据 视角方向 和 顶点法线,计算得到折射向量,使用折射向量从立方体纹理内取值

image​​image

我们同样可以根据光路可逆原则,使用视角方向计算出光线方向

我们在计算折射方向时,会用到斯涅耳定律(Snell’s Law):

η1sinθ1=η2sinθ2\eta_1\sin\theta_1 = \eta_2\sin\theta_2

当光从介质 1 沿着表面法线夹角为 θ1\theta_1 的方向斜射入介质 2 时,
我们可以利用数学公式 η1sinθ1=η2sinθ2\eta_1\sin\theta_1 = \eta_2\sin\theta_2 计算出折射光线和法线的夹角 θ1\theta_1
其中 η1\eta_1η2\eta_2 为两种介质的折射率

对于其中的折射率来说,不同物体的折射率各不相同,我们在制作时,可以在搜索引擎中搜索 常用折射率表 来获取对应物体的折射率

折射效果注意点

我们在 Unity 中处理 折射效果 的做法是:直接用得到的折射方向对立方体纹理进行采样,这样做其实不符合物理规律,
因为对于一个透明物体来说,更准确的模拟方式应该是进行两侧折射,一次是光线进入内部,一次是光线从物体内部射出。
但是,在实时渲染中模拟第二次折射方向较为复杂,而我们仅模拟一次折射得到的效果在视觉上看起来也是可以接受的!

image

因此,在实时渲染中,我们通常仅模拟第一次折射来得到最终的结果!

折射的基础实现

折射计算函数

CG 中提供了内置函数 refract​ 用于进行折射向量的计算

1
refract(入射方向单位向量, 顶点法线单位向量, 入射介质折射率/射入介质折射率)

传入 入射方向单位向量、顶点法线单位向量、入射介质与射入介质折射率比值 便可以得到在目标介质中的折射向量

该函数内部就是利用了斯涅耳定律进行的计算,我们无需再自己写逻辑计算了

折射基础效果实现

  1. 属性声明

    我们将声明4个关键属性:

    1. 介质A折射率
    2. 介质B折射率
    3. 立方体纹理贴图
    4. 折射程度

    指的一提的是,介质A折射率 和 介质B折射率 属性可以合并为 介质A与介质B的折射率比值 属性
    我们在外部计算好这个比值,直接传入即可,这样即可减少计算步骤,提高效率,这部分因为是学习需要,所以还是使用两个属性

    1
    2
    3
    4
    5
    6
    7
    Properties
    {
    _RefractiveIndexA("RefractiveIndexA", Range(1, 2)) = 1 // 介质A折射率
    _RefractiveIndexB("RefractiveIndexA", Range(1, 2)) = 1.3 // 介质B折射率
    _Cube("Cubemap", Cube) = ""{} // 立方体纹理贴图
    _RefractAmount("RefractAmount", Range(0, 1)) = 1 // 折射程度
    }
  2. 编译指令、内置文件、属性映射、结构体相关

    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
    SubShader
    {
    Tags { "RenderType"="Opaque" "Queue"="Geometry" }

    Pass
    {
    CGPROGRAM
    #pragma vertex vert
    #pragma fragment frag

    #include "UnityCG.cginc"

    samplerCUBE _Cube;
    fixed _RefractiveIndexA;
    fixed _RefractiveIndexB;
    fixed _RefractAmount;

    struct v2f
    {
    float4 pos: SV_POSITION; // 裁剪空间下顶点坐标
    float3 worldRefr: TEXCOORD0; // 折射向量
    };

    // ...
    ENDCG
    }
    }

  3. 顶点着色器

    关键步骤:

    1. 顶点坐标转裁剪坐标
    2. 顶点法线转世界坐标
    3. 顶点坐标转世界坐标
    4. 世界空间下视角方向计算
    5. 利用折射函数计算折射向量
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    v2f vert(appdata_base v)
    {
    v2f data;
    data.pos = UnityObjectToClipPos(v.vertex); // 顶点坐标转裁剪坐标
    fixed3 worldNormal = UnityObjectToWorldNormal(v.normal); // 顶点法线转世界法线
    fixed3 worldPos = mul(unity_ObjectToWorld, v.vertex).xyz; // 顶点坐标转世界坐标
    fixed3 worldViewDir = UnityWorldSpaceViewDir(worldPos); // 世界空间下视角方向的计算
    // 根据逆向的视角方向,法线向量,两个介质的折射率比值计算折射向量
    data.worldRefr = refract(-normalize(worldViewDir), normalize(worldNormal), _RefractiveIndexA / _RefractiveIndexB);

    return data;
    }
  4. 片元着色器

    关键步骤:

    1. 立方体纹理采样(利用 texCUBE​ 函数)
    2. 结合折射程度返回最终颜色
    1
    2
    3
    4
    5
    fixed4 frag(v2f i): SV_TARGET
    {
    fixed4 cubemapColor = texCUBE(_Cube, i.worldRefr); // 立方体纹理采样
    return cubemapColor * _RefractAmount; // 结合折射程度计算最终颜色
    }

显示效果:

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
Shader "Unlit/Lesson77_RefractionBase"
{
Properties
{
_RefractiveIndexA("RefractiveIndexA", Range(1, 2)) = 1 // 介质1折射率
_RefractiveIndexB("RefractiveIndexA", Range(1, 2)) = 1.3 // 介质2折射率
_Cube("Cubemap", Cube) = ""{} // 立方体纹理贴图
_RefractAmount("RefractAmount", Range(0, 1)) = 1 // 折射程度
}

SubShader
{
Tags { "RenderType"="Opaque" "Queue"="Geometry" }

Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag

#include "UnityCG.cginc"

samplerCUBE _Cube;
fixed _RefractiveIndexA;
fixed _RefractiveIndexB;
fixed _RefractAmount;

struct v2f
{
float4 pos: SV_POSITION; // 裁剪空间下顶点坐标
float3 worldRefr: TEXCOORD0; // 折射向量
};

v2f vert(appdata_base v)
{
v2f data;
data.pos = UnityObjectToClipPos(v.vertex); // 顶点坐标转裁剪坐标
fixed3 worldNormal = UnityObjectToWorldNormal(v.normal); // 顶点法线转世界法线
fixed3 worldPos = mul(unity_ObjectToWorld, v.vertex).xyz; // 顶点坐标转世界坐标
fixed3 worldViewDir = UnityWorldSpaceViewDir(worldPos); // 世界空间下视角方向的计算
// 根据逆向的视角方向,法线向量,两个介质的折射率比值计算折射向量
data.worldRefr = refract(-normalize(worldViewDir), normalize(worldNormal), _RefractiveIndexA / _RefractiveIndexB);

return data;
}

fixed4 frag(v2f i): SV_TARGET
{
fixed4 cubemapColor = texCUBE(_Cube, i.worldRefr); // 立方体纹理采样
return cubemapColor * _RefractAmount; // 结合折射程度计算最终颜色
}

ENDCG
}
}
}

实现带漫反射和阴影的折射效果

  1. 复用上文的基础折射 Shader 代码

  2. 折射效果结合漫反射和阴影

    折射效果结合漫反射和阴影和之前 反射 几乎一模一样,我们可以直接复制之前的代码,避免书写重复内容

    1. 属性相关

      添加漫反射颜色和折射颜色,将 折射率A 和 折射率B 合并为一个 折射率比值变量
      折射率比值变量可以以后在外部算好了再传入,避免内部计算浪费性能

      1
      2
      3
      4
      5
      6
      7
      8
      Properties
      {
      _Color("Color", Color) = (1, 1, 1, 1) // 漫反射颜色
      _RefractColor("RefractColor", Color) = (1, 1, 1, 1) // 折射颜色
      _RefractRatio("RefractRatio", Range(0.1, 1)) = 0.5 // 两个介质的折射率比值
      _Cube("Cubemap", Cube) = ""{} // 立方体纹理贴图
      _RefractAmount("RefractAmount", Range(0, 1)) = 1 // 折射程度
      }
    2. 渲染路径、编译指令、内置文件复制、属性映射复制修改

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      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;
      fixed4 _RefractColor;
      samplerCUBE _Cube;
      fixed _RefractRatio;
      fixed _RefractAmount;
      // ...
      }
    3. 结构体相关内容复制

      1
      2
      3
      4
      5
      6
      7
      8
      struct v2f
      {
      float4 pos: SV_POSITION; // 裁剪空间下的顶点坐标
      fixed3 worldNormal: NORMAL; // 世界空间下法线
      float3 worldPos: TEXCOORD0; // 世界空间下顶点坐标
      float3 worldRefr: TEXCOORD1; // 折射向量
      SHADOW_COORDS(2) // 阴影坐标宏
      };
    4. 顶点着色器相关内容修改

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      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; // 顶点坐标转世界坐标
      fixed3 worldViewDir = UnityWorldSpaceViewDir(data.worldPos); // 世界空间下视角方向的计算
      // 根据逆向的视角方向,法线向量,两个介质的折射率比值计算折射向量
      data.worldRefr = refract(-normalize(worldViewDir), normalize(data.worldNormal), _RefractRatio);
      TRANSFER_SHADOW(data) // 阴影转换计算

      return data;
      }
    5. 片元着色器相关内容复制修改

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      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.worldRefr).rgb * _RefractColor.rgb;
      // 计算光照衰减和阴影相关的衰减值
      UNITY_LIGHT_ATTENUATION(atten, i, i.worldPos)
      // 利用插值计算,在 漫反射颜色 和 反射颜色 之间进行插值,0和1就是极限状态,0代表没有反射效果,1代表只要反射效果,0~1就是两者的叠加
      fixed3 color = UNITY_LIGHTMODEL_AMBIENT.rgb + lerp(diffuse, cubemapColor, _RefractAmount) * atten;
      return fixed4(color, 1);
      }
    6. 添加 FallBack "Reflective/VertexLit"​,实现投射阴影

      1
      2
      3
      4
      5
      6
      7
      Shader "Unlit/Lesson78_Refraction"
      {
      Properties {/*...*/}
      SubShader {/*...*/}

      Fallback "Reflective/VertexLit"
      }

显示效果(左为结合了漫反射光照和阴影效果的折射Shader,折射比值是0.76,右为纯折射Shader):

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
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
Shader "Unlit/Lesson78_Refraction"
{
Properties
{
_Color("Color", Color) = (1, 1, 1, 1) // 漫反射颜色
_RefractColor("RefractColor", Color) = (1, 1, 1, 1) // 折射颜色
_RefractRatio("RefractRatio", Range(0.1, 1)) = 0.5 // 两个介质的折射率比值
_Cube("Cubemap", Cube) = ""{} // 立方体纹理贴图
_RefractAmount("RefractAmount", 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;
fixed4 _RefractColor;
samplerCUBE _Cube;
fixed _RefractRatio;
fixed _RefractAmount;

struct v2f
{
float4 pos: SV_POSITION; // 裁剪空间下的顶点坐标
fixed3 worldNormal: NORMAL; // 世界空间下法线
float3 worldPos: TEXCOORD0; // 世界空间下顶点坐标
float3 worldRefr: TEXCOORD1; // 折射向量
SHADOW_COORDS(2) // 阴影坐标宏
};

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; // 顶点坐标转世界坐标
fixed3 worldViewDir = UnityWorldSpaceViewDir(data.worldPos); // 世界空间下视角方向的计算
// 根据逆向的视角方向,法线向量,两个介质的折射率比值计算折射向量
data.worldRefr = refract(-normalize(worldViewDir), normalize(data.worldNormal), _RefractRatio);
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.worldRefr).rgb * _RefractColor.rgb;
// 计算光照衰减和阴影相关的衰减值
UNITY_LIGHT_ATTENUATION(atten, i, i.worldPos)
// 利用插值计算,在 漫反射颜色 和 反射颜色 之间进行插值,0和1就是极限状态,0代表没有反射效果,1代表只要反射效果,0~1就是两者的叠加
fixed3 color = UNITY_LIGHTMODEL_AMBIENT.rgb + lerp(diffuse, cubemapColor, _RefractAmount) * atten;
return fixed4(color, 1);
}

ENDCG
}
}

Fallback "Reflective/VertexLit"
}