US3S8L9——玻璃效果

玻璃效果

玻璃效果基本原理

根据我们目前学习的知识,想要实现玻璃效果,可能会联想到使用透明相关知识来进行制作。
虽然透明固然可以制作出玻璃透明的效果,但是它在许多地方有所缺陷,比如:

  1. 透明无法表现出复杂的光学效果

    玻璃不仅仅是透明的,它还具有反射、折射等光学效果,使用透明无法简单的实现这些效果

  2. 透明物体往往会遇到深度排序问题

    渲染顺序不正确时,会导致视觉错误

等等

因此我们想要实现效果更好的玻璃效果时,往往不会选择使用透明来制作

因此我们将使用渲染纹理来制作玻璃效果,基本原理是:

渲染玻璃效果物体之前,先获取到当前屏幕图像,将当前屏幕图像存储在渲染纹理之中
之后在真正处理玻璃效果物体时,再 利用该渲染纹理 来实现 透明、折射 等等效果,
该过程中并不会使用混合相关知识,而是直接进行颜色相乘或相加来进行颜色叠加

imageimageimage

也就是说,这里的透明效果本质上是通过把 物体遮挡后边的屏幕内容的部分 叠加到自己身上,
使得可以从物体身上看到被物体遮挡的后面的内容,以模拟出一种透明的效果,
在此基础上,就可以叠加显示物体本身颜色和反射等效果

一句话总结:在渲染玻璃效果之前,先捕获当前屏幕内容并保存到一张渲染纹理当中,
在之后的 Shader 处理中利用该渲染纹理进行采样,参与最终的颜色计算,实现各种玻璃效果。

玻璃效果实现使用的知识点

特殊渲染通道 GrabPass

GrabPass​ 的作用是捕获当前屏幕上已经渲染的内容,并将其存储到一张纹理中,它需要包含在 SubShader​ 语句块中

它的用法有两种:

  1. 大括号中什么都不写,默认会把屏幕内容写入一个叫做 _GrabTexture​ 的纹理变量中
    直接在 CG 语句中声明 _GrabTexture​ 纹理变量即可直接使用抓取的渲染纹理

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    SubShader
    {
    GrabPass {}

    Pass
    {
    CGPROGRAM
    #pragma vertex vert
    #pragma fragment frag
    #include "UnityCG.cginc"

    sampler2D _GrabTexture;
    // ...
    }
    }
  2. 大括号中写入自定义变量名,会把对应屏幕内容写入该自定义纹理变量中
    在 CG 语句中声明对应纹理变量即可使用抓取的渲染纹理

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    SubShader
    {
    GrabPass { "_RefractionTex" }

    Pass
    {
    CGPROGRAM
    #pragma vertex vert
    #pragma fragment frag
    #include "UnityCG.cginc"

    sampler2D _RefractionTex;
    // ...
    }
    }

内置函数 ComputeGrabScreenPos

该内置函数可以用于计算屏幕空间位置,传入顶点的裁剪空间位置
返回一个 float4​ 结果,该 float4​ 中的内容分别代表:

  • X:屏幕空间 X 坐标
  • Y:屏幕空间 Y 坐标
  • Z:裁剪空间深度值,一般表示顶点距离摄像机的相对深度
  • W:裁剪空间的 W 分量,通常用于透视除法,即 X 或 Y/W 后 X 或 Y 的范围将在0~1之间
1
2
3
4
5
6
v2f vert(appdata_base v)
(
v2f data;
data.pos = UnityObjectToClipPos(v.vertex); // 顶点坐标转裁剪坐标
data.grabPos = ComputeScreenPos(data.pos); // 将裁剪坐标转换到屏幕坐标
}

我们可以利用该函数得到顶点相对屏幕的坐标,从而从捕获的渲染纹理中进行采样

如果要通过此坐标从屏幕纹理内采样得到纹理,需要先获取先进行计算:

1
fixed2 screenUV = i.grabPos.xy / i.grabPos.w;    // 利用透视除法,将屏幕坐标转换到0~1范围内,然后再从屏幕纹理内采样

模拟折射的自定义计算规则

为了模拟出玻璃折射的效果,我们一般不会使用之前学习的折射相关知识点在立方体纹理中采样,
我们往往会自定义一些计算规则,来模拟计算出折射的效果。
总体的设计思路,就是在对捕获纹理进行采样时,进行一些偏移计算。
具体的偏移计算规则,完全是可以自定义的,取决于你如何计算

测试场景

创建如下的一个场景,存在六面带纹理和法线的墙包围起来的房间(墙只开单面阴影),其中包含两个立方体
内层的立方体作为展示,外层的立方体使用玻璃效果Shader,要求外层的立方体能够看到内层立方体且实现玻璃效果

image

玻璃效果基础实现

如何让玻璃效果对象滞后渲染

在实现玻璃效果之前,需要先捕获当前屏幕内容并保存到一张渲染纹理当中,
那么要保证玻璃效果对象后面的内容正确渲染,我们必须保证玻璃对象能够滞后渲染

想要让一个对象滞后渲染,那么通过我们学习过的知识,自然的联想到了渲染标签 Tags​ 中的 渲染队列 Queue​

具体内容详见:US2S2L5-1——Tags-渲染标签

因此对于玻璃效果对象,虽然它本质上是一个不透明物体,但是我们完全可以将它的渲染队列设置为 Transparent​ (透明的)
保证它晚于 背景队列、几何队列、透明测试队列 之后再进行渲染,这时我们捕获的屏幕内容,将包含这些更早渲染的内容信息
便可以利用 GrabPass​ 捕获到相对正确的内容了

基础玻璃效果实现

  1. 新建一个Shader,复用 反射基础实现 中的代码

    详见:US3S8L4——反射效果

  2. 修改相关代码

    1. 修改属性代码(同时修改CG中的属性映射)

      1. 加入主纹理属性(用于处理物体本身颜色)
      2. 加入立方体纹理属性(用于处理反射)
      3. 将反射率改为折射程度(用于控制折射程度:0 表示完全不折射,相当于完全反射,1 表示完全折射,相当于完全透明)
      1
      2
      3
      4
      5
      6
      Properties
      {
      _MainTex("MainTex", 2D) = ""{} // 主纹理
      _Cube("Cubemap", Cube) = ""{} // 立方体纹理
      _RefractAmount("RefractAmount", Range(0, 1)) = 1 // 折射程度,0表示完全反射,1表示完全透明
      }
    2. 修改渲染队列为 Transparent

      但是 RenderType​ 渲染类型不修改,因为它本质上还是一个不透明物体,以后使用着色器替换功能时,可以再被正常渲染

      1
      2
      3
      4
      5
      6
      SubShader
      {
      Tags { "RenderType" = "Opaque" "Queue" = "Transparent" }

      Pass { /*...*/ }
      }
    3. 加入 GrabPass​,抓取屏幕图像存储渲染纹理

      1
      2
      3
      4
      5
      SubShader
      {
      Tags { "RenderType" = "Opaque" "Queue" = "Transparent" }
      GrabPass {} // 捕获渲染此物体之前当前的屏幕内容,并存储到默认的渲染纹理变量内
      }
    4. 修改 v2f​ 结构体

      1. 加入相对屏幕坐标 float4​ 类型成员
      2. 加入 uv​,用于采样物体颜色纹理
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      sampler2D _MainTex;
      float4 _MainTex_ST;
      samplerCUBE _Cube;
      float _RefractAmount;
      sampler2D _GrabTexture; // GrabPass 默认存储的纹理变量

      struct v2f
      {
      float4 pos: SV_POSITION; // 裁剪空间下的顶点坐标
      float4 grabPos: TEXCOORD0; // 用于存储从屏幕图像中采样的坐标
      float2 uv: TEXCOORD1; // 用于在颜色纹理中采样的UV坐标
      float3 worldReflection: TEXCOORD2; // 世界空间下的反射向量
      };
    5. 修改顶点着色器

      使用 ComputeGrabScreenPos​ 方法,计算结构体中相对屏幕坐标,计算纹理采样 uv​ 坐标的缩放偏移

      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.grabPos = ComputeScreenPos(data.pos); // 将裁剪坐标转换到屏幕坐标
      data.uv.xy = v.texcoord.xy * _MainTex_ST.xy + _MainTex_ST.zw; // uv坐标计算
      float3 worldNormal = UnityObjectToWorldNormal(v.normal); // 顶点法线转世界坐标
      fixed3 worldPos = mul(unity_ObjectToWorld, v.vertex).xyz; // 顶点坐标装世界坐标
      fixed3 worldViewDir = UnityWorldSpaceViewDir(worldPos); // 计算世界空间下视角所在方向
      data.worldReflection = reflect(-worldViewDir, worldNormal); // 通过反向视角方向得到反射方向

      return data;
      }
    6. 修改片元着色器

      • uv​ 采样主纹理颜色,将屏幕坐标转为裁剪坐标 0~1 范围内,对捕获纹理进行采样
      • 用反射在立方体纹理中进行采样,用结果乘以主纹理颜色,进行颜色叠加
      • 用折射程度参与最终的颜色计算,折射程度值的变化决定了最终表现效果在 完全反射 到 完全折射 之间变化
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      fixed4 frag(v2f i): SV_TARGET
      {
      // 把立方体反射纹理采样颜色叠加到物体主纹理采样颜色上,得到反射颜色
      fixed4 mainTex = tex2D(_MainTex, i.uv); // 物体主纹理采样颜色
      fixed4 reflectColor = texCUBE(_Cube, i.worldReflection) * mainTex;
      // 折射颜色获取(即从物体遮挡的后边的屏幕内容采样,获取类似透明效果的颜色)
      float2 screenUV = i.grabPos.xy / i.grabPos.w; // 利用透视除法,将屏幕坐标转换到0~1范围内,然后再从屏幕纹理内采样
      fixed4 grabColor = tex2D(_GrabTexture, screenUV);
      // 通过折射程度,控制反射颜色和屏幕颜色的叠加,得到最终颜色
      fixed4 color = reflectColor * (1 - _RefractAmount) + grabColor * _RefractAmount;
      return color;
      }

显示效果(折射程度0.4):

image

可以看到,现在的玻璃效果既有透明效果,也能看到玻璃效果物体本身的颜色和反射的颜色,
不过目前并没有一个折射效果,因为我们还没做折射颜色的偏移,导致目前的折射颜色只有透明效果

自定义折射效果

目前我们利用屏幕坐标在抓取纹理中采样并没有处理偏移,因此呈现出来的效果像是在透明和半透明之前切换
我们可以自定义一些简单的偏移计算规则,让最终的采样位置发生偏移,模拟折射效果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
fixed4 frag(v2f i): SV_TARGET
{
// 把立方体反射纹理采样颜色叠加到物体主纹理采样颜色上,得到反射颜色
fixed4 mainTex = tex2D(_MainTex, i.uv); // 物体主纹理采样颜色
fixed4 reflectColor = texCUBE(_Cube, i.worldReflection) * mainTex;
// 折射颜色获取(即从物体遮挡的后边的屏幕内容采样,获取类似透明效果的颜色)
float2 offset = (1 - _RefractAmount) / 10; // 在采样前计算xy屏幕坐标的偏移量
i.grabPos.xy = i.grabPos.xy - offset; // 对xy屏幕坐标进行偏移
float2 screenUV = i.grabPos.xy / i.grabPos.w; // 利用透视除法,将屏幕坐标转换到0~1范围内,然后再从屏幕纹理内采样
fixed4 grabColor = tex2D(_GrabTexture, screenUV);
// 通过折射程度,控制反射颜色和屏幕颜色的叠加,得到最终颜色
fixed4 color = reflectColor * (1 - _RefractAmount) + grabColor * _RefractAmount;
return color;
}

其中,offset​ 的计算完全是可以根据自己的需要来自定义计算规则

显示效果(折射程度0.4):

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
Shader "TeachShader/Lesson85_GressBase"
{
Properties
{
_MainTex("MainTex", 2D) = ""{} // 主纹理
_Cube("Cubemap", Cube) = ""{} // 立方体纹理
_RefractAmount("RefractAmount", Range(0, 1)) = 1 // 折射程度,0表示完全反射,1表示完全透明
}
SubShader
{
Tags { "RenderType" = "Opaque" "Queue" = "Transparent" }

GrabPass {} // 捕获渲染此物体之前当前的屏幕内容,并存储到默认的渲染纹理变量内

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

CGPROGRAM
#pragma vertex vert
#pragma fragment frag

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

sampler2D _MainTex;
float4 _MainTex_ST;
samplerCUBE _Cube;
float _RefractAmount;
sampler2D _GrabTexture; // GrabPass 默认存储的纹理变量

struct v2f
{
float4 pos: SV_POSITION; // 裁剪空间下的顶点坐标
float4 grabPos: TEXCOORD0; // 用于存储从屏幕图像中采样的坐标
float2 uv: TEXCOORD1; // 用于在颜色纹理中采样的UV坐标
float3 worldReflection: TEXCOORD2; // 世界空间下的反射向量
};

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

return data;
}

fixed4 frag(v2f i): SV_TARGET
{
// 把立方体反射纹理采样颜色叠加到物体主纹理采样颜色上,得到反射颜色
fixed4 mainTex = tex2D(_MainTex, i.uv); // 物体主纹理采样颜色
fixed4 reflectColor = texCUBE(_Cube, i.worldReflection) * mainTex;
// 折射颜色获取(即从物体遮挡的后边的屏幕内容采样,获取类似透明效果的颜色)
float2 offset = (1 - _RefractAmount) / 10; // 在采样前计算xy屏幕坐标的偏移量
i.grabPos.xy = i.grabPos.xy - offset; // 对xy屏幕坐标进行偏移
float2 screenUV = i.grabPos.xy / i.grabPos.w; // 利用透视除法,将屏幕坐标转换到0~1范围内,然后再从屏幕纹理内采样
fixed4 grabColor = tex2D(_GrabTexture, screenUV);
// 通过折射程度,控制反射颜色和屏幕颜色的叠加,得到最终颜色
fixed4 color = reflectColor * (1 - _RefractAmount) + grabColor * _RefractAmount;
return color;
}
ENDCG
}
}
}

带法线纹理的玻璃效果

顾名思义,我们希望通过玻璃透明效果带有凹凸感,就需要借助法线纹理

带法线纹理的玻璃效果实现

  1. 创建 Shader,复用上文的玻璃效果基础实现的相关代码

  2. BumpedDiffuse​ 标准法线漫反射Shader 中关于法线相关的计算整合进来

    标准法线漫反射 Shader 内容详见:US3S7L1——标准漫反射Shader

    注意:不再需要 _BumpScale​ 来控制凹凸程度,我们默认法线凹凸程度最大化

  3. 修改反射向量计算规则

    由于法线需要从法线纹理中获取,因此需要将反射向量的计算放入到片元着色器中

    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
    sampler2D _MainTex;     // 颜色纹理
    float4 _MainTex_ST; // 颜色纹理的缩放和平移
    sampler2D _BumpMap; // 法线纹理
    float4 _BumpMap_ST; // 法线纹理的缩放和平移
    samplerCUBE _Cube; // 反射用的立方体纹理
    float _RefractAmount; // 折射程度
    sampler2D _GrabTexture; // GrabPass 默认存储的纹理变量

    struct v2f
    {
    float4 pos: SV_POSITION; // 裁剪空间下的顶点坐标
    float4 grabPos: TEXCOORD0; // 用于存储从屏幕图像中采样的坐标
    float4 uv: TEXCOORD1; // 用于在颜色纹理(xy)和法线纹理(zw)中采样的UV坐标
    float4 tangentToWorld0: TEXCOORD3; // 它用来存储变换矩阵和顶点相对于世界坐标的位置的第一行
    float4 tangentToWorld1: TEXCOORD4; // 它用来存储变换矩阵和顶点相对于世界坐标的位置的第二行
    float4 tangentToWorld2: TEXCOORD5; // 它用来存储变换矩阵和顶点相对于世界坐标的位置的第三行
    };

    v2f vert(appdata_full v)
    {
    v2f data;
    data.pos = UnityObjectToClipPos(v.vertex); // 顶点坐标转裁剪坐标
    data.grabPos = ComputeScreenPos(data.pos); // 将裁剪坐标转换到屏幕坐标
    data.uv.xy = v.texcoord.xy * _MainTex_ST.xy + _MainTex_ST.zw; // 颜色纹理uv坐标计算
    data.uv.zw = v.texcoord.xy * _BumpMap_ST.xy + _BumpMap_ST.zw; // 法线纹理uv坐标计算
    float3 worldNormal = UnityObjectToWorldNormal(v.normal); // 顶点法线转世界坐标
    fixed3 worldPos = mul(unity_ObjectToWorld, v.vertex).xyz; // 顶点坐标装世界坐标
    // 将模型空间下的切线转换到世界空间下,并计算世界空间下的副切线
    float3 worldTangent = UnityObjectToWorldDir(v.tangent);
    float3 worldBinormal = cross(normalize(worldTangent), normalize(worldNormal)) * v.tangent.w;
    // 将切线空间到世界空间的转换矩阵,以及世界坐标存储到三个贴图变量内
    data.tangentToWorld0 = float4(worldTangent.x, worldBinormal.x, worldNormal.x, worldPos.x);
    data.tangentToWorld1 = float4(worldTangent.y, worldBinormal.y, worldNormal.y, worldPos.y);
    data.tangentToWorld2 = float4(worldTangent.z, worldBinormal.z, worldNormal.z, worldPos.z);

    return data;
    }

    fixed4 frag(v2f i): SV_TARGET
    {
    // 计算世界空间下视角方向
    float3 worldPos = float3(i.tangentToWorld0.w, i.tangentToWorld1.w, i.tangentToWorld2.w);
    fixed3 viewDir = normalize(UnityWorldSpaceViewDir(worldPos));
    // 通过法线纹理采样并解压缩,得到切线空间下法线数据
    float4 packedNormal = tex2D(_BumpMap, i.uv.zw);
    float3 tangentNormal = UnpackNormal(packedNormal);
    // 将切线空间下法线数据转换到世界空间下
    float3 worldNormal = float3(
    dot(i.tangentToWorld0.xyz, tangentNormal),
    dot(i.tangentToWorld1.xyz, tangentNormal),
    dot(i.tangentToWorld2.xyz, tangentNormal)
    );
    // 根据逆向视角方向和法线纹理内得到的法线纹理贴图计算反射向量
    float3 reflection = reflect(-viewDir, worldNormal);

    // 把立方体反射纹理采样颜色叠加到物体主纹理采样颜色上,得到反射颜色
    fixed4 mainTex = tex2D(_MainTex, i.uv); // 物体主纹理采样颜色
    fixed4 reflectColor = texCUBE(_Cube, reflection) * mainTex;
    // 折射颜色获取(即从物体遮挡的后边的屏幕内容采样,获取类似透明效果的颜色)
    float2 offset = (1 - _RefractAmount) / 10; // 在采样前计算xy屏幕坐标的偏移量
    i.grabPos.xy = i.grabPos.xy - offset; // 对xy屏幕坐标进行偏移
    float2 screenUV = i.grabPos.xy / i.grabPos.w; // 利用透视除法,将屏幕坐标转换到0~1范围内,然后再从屏幕纹理内采样
    fixed4 grabColor = tex2D(_GrabTexture, screenUV);
    // 通过折射程度,控制反射颜色和屏幕颜色的叠加,得到最终颜色
    fixed4 color = reflectColor * (1 - _RefractAmount) + grabColor * _RefractAmount;
    return color;
    }

显示效果(折射程度0.4):

image

可见,现在的玻璃效果就有凹凸感了,但是玻璃的透明效果没有受到凹凸感的影响(没有因法线贴图而扭曲),
因此这里的玻璃效果的折射偏移还需要进一步的结合法线去处理

利用切线空间法线来计算折射偏移

  1. 加入一个控制折射扭曲程度的新属性 _Distortion​ 取值范围可以大一些

  2. 利用切线空间下法线来计算偏移值

    在片元着色器上加入两行关键代码

    • 第一行:float2 offset = tangentNormal.xy * _Distortion;

      使用 切线空间下法线的xy * 扭曲值​ 得到一个偏移量
      代表光线经过法线方向扰动后的偏移程度,确定光线折射的方向和强度

    • 第二行:屏幕坐标.xy = offset * 屏幕坐标.z + 屏幕坐标.xy;

      用偏移量和屏幕空间深度值相乘,模拟出真实的折射效果
      深度值越大(即距离相机越远),折射效果越明显。
      这样可以实现近大远小的效果,使得物体在不同深度上的折射效果有所差异。

    这种计算方式是计算机图形学前辈们通过实践总结出来的接近真实世界折射效果的方法

    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
    fixed4 frag(v2f i): SV_TARGET
    {
    // 计算世界空间下视角方向
    float3 worldPos = float3(i.tangentToWorld0.w, i.tangentToWorld1.w, i.tangentToWorld2.w);
    fixed3 viewDir = normalize(UnityWorldSpaceViewDir(worldPos));
    // 通过法线纹理采样并解压缩,得到切线空间下法线数据
    float4 packedNormal = tex2D(_BumpMap, i.uv.zw);
    float3 tangentNormal = UnpackNormal(packedNormal);
    // 将切线空间下法线数据转换到世界空间下
    float3 worldNormal = float3(
    dot(i.tangentToWorld0.xyz, tangentNormal),
    dot(i.tangentToWorld1.xyz, tangentNormal),
    dot(i.tangentToWorld2.xyz, tangentNormal)
    );
    // 根据逆向视角方向和法线纹理内得到的法线纹理贴图计算反射向量
    float3 reflection = reflect(-viewDir, worldNormal);

    // 把立方体反射纹理采样颜色叠加到物体主纹理采样颜色上,得到反射颜色
    fixed4 mainTex = tex2D(_MainTex, i.uv); // 物体主纹理采样颜色
    fixed4 reflectColor = texCUBE(_Cube, reflection) * mainTex;
    // 折射颜色获取(即从物体遮挡的后边的屏幕内容采样,获取类似透明效果的颜色)
    float2 offset = tangentNormal.xy * _Distortion; // 在采样前计算xy屏幕坐标的偏移量
    i.grabPos.xy = offset * i.grabPos.z + i.grabPos.xy; // 用偏移量和屏幕空间深度值相乘,模拟出真实的折射效果
    float2 screenUV = i.grabPos.xy / i.grabPos.w; // 利用透视除法,将屏幕坐标转换到0~1范围内,然后再从屏幕纹理内采样
    fixed4 grabColor = tex2D(_GrabTexture, screenUV);
    // 通过折射程度,控制反射颜色和屏幕颜色的叠加,得到最终颜色
    fixed4 color = reflectColor * (1 - _RefractAmount) + grabColor * _RefractAmount;
    return color;
    }

显示效果(折射程度0.5,扭曲程度3):

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
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
Shader "TeachShader/Lesson86_GressRefraction"
{
Properties
{
_MainTex("MainTex", 2D) = ""{} // 主纹理
_BumpMap("BumpMap", 2D) = ""{} // 法线纹理
_Cube("Cubemap", Cube) = ""{} // 立方体纹理
_RefractAmount("RefractAmount", Range(0, 1)) = 1 // 折射程度,0表示完全反射,1表示完全透明
_Distortion("Distortion", Range(0, 10)) = 0 // 控制扭曲程度的变量
}
SubShader
{
Tags { "RenderType" = "Opaque" "Queue" = "Transparent" }

GrabPass {} // 捕获渲染此物体之前当前的屏幕内容,并存储到默认的渲染纹理变量内

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

CGPROGRAM
#pragma vertex vert
#pragma fragment frag

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

sampler2D _MainTex; // 颜色纹理
float4 _MainTex_ST; // 颜色纹理的缩放和平移
sampler2D _BumpMap; // 法线纹理
float4 _BumpMap_ST; // 法线纹理的缩放和平移
samplerCUBE _Cube; // 反射用的立方体纹理
float _RefractAmount; // 折射程度
sampler2D _GrabTexture; // GrabPass 默认存储的纹理变量
float _Distortion; // 扭曲程度

struct v2f
{
float4 pos: SV_POSITION; // 裁剪空间下的顶点坐标
float4 grabPos: TEXCOORD0; // 用于存储从屏幕图像中采样的坐标
float4 uv: TEXCOORD1; // 用于在颜色纹理(xy)和法线纹理(zw)中采样的UV坐标
float4 tangentToWorld0: TEXCOORD3; // 它用来存储变换矩阵和顶点相对于世界坐标的位置的第一行
float4 tangentToWorld1: TEXCOORD4; // 它用来存储变换矩阵和顶点相对于世界坐标的位置的第二行
float4 tangentToWorld2: TEXCOORD5; // 它用来存储变换矩阵和顶点相对于世界坐标的位置的第三行
};

v2f vert(appdata_full v)
{
v2f data;
data.pos = UnityObjectToClipPos(v.vertex); // 顶点坐标转裁剪坐标
data.grabPos = ComputeScreenPos(data.pos); // 将裁剪坐标转换到屏幕坐标
data.uv.xy = v.texcoord.xy * _MainTex_ST.xy + _MainTex_ST.zw; // 颜色纹理uv坐标计算
data.uv.zw = v.texcoord.xy * _BumpMap_ST.xy + _BumpMap_ST.zw; // 法线纹理uv坐标计算
float3 worldNormal = UnityObjectToWorldNormal(v.normal); // 顶点法线转世界坐标
fixed3 worldPos = mul(unity_ObjectToWorld, v.vertex).xyz; // 顶点坐标装世界坐标
// 将模型空间下的切线转换到世界空间下,并计算世界空间下的副切线
float3 worldTangent = UnityObjectToWorldDir(v.tangent);
float3 worldBinormal = cross(normalize(worldTangent), normalize(worldNormal)) * v.tangent.w;
// 将切线空间到世界空间的转换矩阵,以及世界坐标存储到三个贴图变量内
data.tangentToWorld0 = float4(worldTangent.x, worldBinormal.x, worldNormal.x, worldPos.x);
data.tangentToWorld1 = float4(worldTangent.y, worldBinormal.y, worldNormal.y, worldPos.y);
data.tangentToWorld2 = float4(worldTangent.z, worldBinormal.z, worldNormal.z, worldPos.z);

return data;
}

fixed4 frag(v2f i): SV_TARGET
{
// 计算世界空间下视角方向
float3 worldPos = float3(i.tangentToWorld0.w, i.tangentToWorld1.w, i.tangentToWorld2.w);
fixed3 viewDir = normalize(UnityWorldSpaceViewDir(worldPos));
// 通过法线纹理采样并解压缩,得到切线空间下法线数据
float4 packedNormal = tex2D(_BumpMap, i.uv.zw);
float3 tangentNormal = UnpackNormal(packedNormal);
// 将切线空间下法线数据转换到世界空间下
float3 worldNormal = float3(
dot(i.tangentToWorld0.xyz, tangentNormal),
dot(i.tangentToWorld1.xyz, tangentNormal),
dot(i.tangentToWorld2.xyz, tangentNormal)
);
// 根据逆向视角方向和法线纹理内得到的法线纹理贴图计算反射向量
float3 reflection = reflect(-viewDir, worldNormal);

// 把立方体反射纹理采样颜色叠加到物体主纹理采样颜色上,得到反射颜色
fixed4 mainTex = tex2D(_MainTex, i.uv); // 物体主纹理采样颜色
fixed4 reflectColor = texCUBE(_Cube, reflection) * mainTex;
// 折射颜色获取(即从物体遮挡的后边的屏幕内容采样,获取类似透明效果的颜色)
float2 offset = tangentNormal.xy * _Distortion; // 在采样前计算xy屏幕坐标的偏移量
i.grabPos.xy = offset * i.grabPos.z + i.grabPos.xy; // 用偏移量和屏幕空间深度值相乘,模拟出真实的折射效果
float2 screenUV = i.grabPos.xy / i.grabPos.w; // 利用透视除法,将屏幕坐标转换到0~1范围内,然后再从屏幕纹理内采样
fixed4 grabColor = tex2D(_GrabTexture, screenUV);
// 通过折射程度,控制反射颜色和屏幕颜色的叠加,得到最终颜色
fixed4 color = reflectColor * (1 - _RefractAmount) + grabColor * _RefractAmount;
return color;
}
ENDCG
}
}
}