Sorry, your browser cannot access this site
This page requires browser support (enable) JavaScript
Learn more >

玻璃效果

一、基本原理

透明可以做出透明的效果,但是它存在很多缺陷

  1. 无法实现复杂的光学效果 玻璃不仅仅是透明的,它还具有反射,折射等光学效果,使用透明无法实现这些效果
  2. 透明物体往往会遇到深度排序的问题 渲染顺序不正确时,会导致视觉错误

所以 我们会使用渲染纹理来制作玻璃效果 基本原理是

  1. 在渲染效果物体纹理之前,获取到当前屏幕图像
  2. 将当前图像储存在渲染纹理之中
  3. 当处理该玻璃效果物体时,再利用储存的渲染纹理实现透明折射的效果

注意: 该纹理中并不会真正的使用混合相关知识,而是直接进行颜色相乘或相加来进行颜色的叠加

二、使用的知识点

  1. 特殊渲染通道 GrabPass GrabPass 的作用就是捕获当前屏幕上已经渲染的内容并存储到一张渲染纹理中 使用方法:

    1. 大括号中什么都不写 默认会把屏幕内容写入一个叫做 _GrabTexture 的纹理变量中 直接写在 CG 语句中声明 _GrabTexture 纹理变量中,即可使用抓取内容
    2. 大括号中写入自定义的变量名字符串,会把对应屏幕内容写入该自定义纹理变量中 在 CG 语句中声明对应纹理变量,即可使用抓取内容
  2. 内置函数 ComputeGrabScreenPos 该内置函数可以计算屏幕空间的位置 所以我们用该函数得到顶点的相对屏幕的坐标,从而从捕获的渲染纹理中进行采样 参数:顶点的裁剪空间的位置 返回值: float4

    • X:屏幕坐标的 X
    • Y:屏幕坐标的 Y
    • Z:裁剪空间的深度,一般用于表示顶点距离摄像机的相对深度
    • W:裁剪空间的 W 分量,通常用于透视除法 即:X/W 或者 Y/W 的范围在 0~1 之间
  3. 模拟内置函数的自定义计算

三、基础实现

1.实现思路

  1. 这里我们基于反射的基础实现进行修改实现
  2. 修改属性相关
    1. 添加主纹理属性(用于处理物体自身的颜色)
    2. 添加立方体纹理(用于处理反射)
    3. 将反射率修改为折射程度(用于控制折射的程度,0 表示完全不折射,1 代表完全折射)
  3. 要让玻璃对象之后渲染 修改 Tags 相关内容 RenderType 还是 Opaque 因为本质上该渲染的物体还是不透明物体 渲染队列 Queue 更改为 Transparent 这样捕获的屏幕内容,将包含这些更早的渲染的内容
  4. 加入 GrabPass 并添加对应的渲染纹理变量
  5. 修改 v2f 结构体
    1. 加入相对于屏幕坐标的 float4 成员
    2. 加入 uv 坐标 用于采样物体的颜色
  6. 修改顶点着色器
    1. 使用 ComputerGrabScreenPos 方法,计算顶点在屏幕坐标的位置
    2. 计算纹理的缩放偏移
  7. 修改片元着色器
    1. 使用 uv 坐标采样主纹理
    2. 将屏幕坐标转为裁剪坐标 (即:使用透视除法 将坐标限制到 0~1 之间),再对捕获的纹理进行采样(这里可以进行自定义偏移,以实现折射的效果)
    3. 用反射再立方体纹理中进行采样,用结果乘以主纹理 并进行颜色叠加
    4. 用折射程度参与最终颜色的计算 折射程度的变化值决定了最终效果的表现(可以使用 lerp 函数进行限制)

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
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
100
101
Shader "Unlit/GlassBase"
{
Properties
{
//主纹理
_MainTex("MainTex",2D) = ""{}
//立方体纹理
_Cube("Cubemap",Cube) = ""{}
//折射程度
//0表示完全反射(完全不折射) 1表示完全不反射(完全折射)
_RefreactAmount("RefreactAmount",Range(0,1)) = 1
}
SubShader
{
Tags{"RenderType" = "Opaque" "Queue" = "Transparent"}
GrabPass{}
Pass
{
//将渲染队列 改为透明的
//目的是让对象之后渲染
//使得能在GrabPass中 正确捕获屏幕图像
Tags{"LightMode" = "ForwardBase"}
//捕获屏幕当前内容 并存储到默认纹理中


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

sampler2D _MainTex;
float4 _MainTex_ST;
samplerCUBE _Cube;
float _RefreactAmount;
//GrabPass中 默认存储的屏幕内容
sampler2D _GrabTexture;
struct v2f
{
float4 pos:SV_POSITION;//裁剪坐标下的顶点坐标
//用于存储从屏幕图像中采样的坐标(顶点相对于屏幕的位置)
fixed4 grabPos:TEXCOORD0;
//用于在颜色纹理中采样的uv坐标
float2 uv:TEXCOORD1;
//世界空间下的顶点坐标
//我们将把反射向量的的计算放在顶点坐标系下 节约性能 表现效果肉眼几乎和片元着色器一致
float3 worldRefl:TEXCOORD2;
};
v2f vert(appdata_base v)
{
v2f o;
//顶点坐标转裁剪坐标
o.pos = UnityObjectToClipPos(v.vertex);
//屏幕坐标转换相关的内容
o.grabPos = ComputeGrabScreenPos(o.pos);
//uv坐标准换的相关内容
o.uv = TRANSFORM_TEX(v.texcoord,_MainTex);

//计算计算反射光向量
//1.计算世界空间下的法线向量
float3 worldNormal = UnityObjectToWorldNormal(v.normal);
//2.计算世界空间下的顶点坐标
fixed3 worldPos = mul(unity_ObjectToWorld,v.vertex).xyz;
//3.计算视角方向 内部是用摄像机位置-世界坐标位置
fixed3 worldViewDir = -UnityWorldSpaceViewDir(worldPos);
//计算反射向量
o.worldRefl = reflect(worldViewDir,worldNormal);
return o;
}
fixed4 frag(v2f i):SV_TARGET
{
//----反射颜色(叠加主纹理颜色)相关计算----
//对主纹理进行采样
fixed4 mainTex = tex2D(_MainTex,i.uv);
//对立方体纹理利用相应的反射函数进行采样 并于主纹理进行叠加
fixed4 reflectColor = texCUBE(_Cube,i.worldRefl) * mainTex;

//----折射颜色相关计算----
//其实就是从我们抓取的 屏幕渲染纹理中进行采样 参与计算
//抓取纹理中的颜色信息 相当于就是这个玻璃对象后面的颜色


//利用透视除法 将屏幕坐标转化到 0~1 范围内 然后再进行采样
//fixed2 screenUV = i.grabPos.xy / i.grabPos.w;
//为了实现折射效果 我们需要将xy的屏幕坐标进行偏移 自定义规则
float2 offset = 1-_RefreactAmount;
i.grabPos.xy = i.grabPos.xy - offset/10;//相当于偏移位置
fixed2 screenUV = i.grabPos.xy / i.grabPos.w;
//从捕获的屏幕纹理中采样 获取后面的颜色
fixed4 grabColor = tex2D(_GrabTexture,screenUV);

//float4 color = reflectColor*(1 - _RefreactAmount) + grabColor * _RefreactAmount;
float4 color = lerp(reflectColor,grabColor,_RefreactAmount);

return color;
}
ENDCG
}
}
}

四、带法线纹理效果实现

1.实现思路

  1. 这里通过改玻璃的基础实现
  2. 整合标准法线漫反射 Shader 中关于法线相关的计算整合进来 注意:不需要_BumpScale 来控制凹凸程度,这里默认最大
  3. 修改反射向量计算规则 由于法线需要从法线纹理中获取 因此需要将反射向量的计算放入片元着色器中

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
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
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
Shader "Unlit/GlassReflaction"
{
Properties
{
//主纹理
_MainTex("MainTex",2D) = ""{}
//法线纹理
_BumpMap("BumpMap",2D) = ""{}
//立方体纹理
_Cube("Cubemap",Cube) = ""{}
//折射程度
//0表示完全反射(完全不折射) 1表示完全不反射(完全折射)
_RefreactAmount("RefreactAmount",Range(0,1)) = 1
//控制折射扭曲程度的变量
_Distortion("Distortion",Range(0,10)) = 0
}
SubShader
{
Tags{"RenderType" = "Opaque" "Queue" = "Transparent"}
GrabPass{}
Pass
{
//将渲染队列 改为透明的
//目的是让对象之后渲染
//使得能在GrabPass中 正确捕获屏幕图像
Tags{"LightMode" = "ForwardBase"}
//捕获屏幕当前内容 并存储到默认纹理中


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

sampler2D _MainTex;
sampler2D _BumpMap;
float4 _MainTex_ST;
float4 _BumpMap_ST;
samplerCUBE _Cube;
float _RefreactAmount;
float _Distortion;
//GrabPass中 默认存储的屏幕内容
sampler2D _GrabTexture;
struct v2f
{
float4 pos:SV_POSITION;//裁剪坐标下的顶点坐标
//用于存储从屏幕图像中采样的坐标(顶点相对于屏幕的位置)
fixed4 grabPos:TEXCOORD0;
//用于在颜色纹理中采样的uv坐标
float4 uv:TEXCOORD1;
//世界空间下的顶点坐标
//我们将把反射向量的的计算放在顶点坐标系下 节约性能 表现效果肉眼几乎和片元着色器一致
//float3 worldRefl:TEXCOORD2;
//代表我们切线空间到世界空间的 变换矩阵的三行
//其中 w 代表顶点相对于世界坐标位置
float4 TtoW0:TEXCOORD2;
float4 TtoW1:TEXCOORD3;
float4 TtoW2:TEXCOORD4;
};
v2f vert(appdata_full v)
{
v2f o;
//顶点坐标转裁剪坐标
o.pos = UnityObjectToClipPos(v.vertex);
//屏幕坐标转换相关的内容
o.grabPos = ComputeGrabScreenPos(o.pos);
//uv坐标准换的相关内容
o.uv.xy = TRANSFORM_TEX(v.texcoord,_MainTex).xy;
//法线纹理的uv坐标计算
o.uv.zw = TRANSFORM_TEX(v.texcoord,_BumpMap).xy;
//计算计算反射光向量
//1.计算世界空间下的法线向量
float3 worldNormal = UnityObjectToWorldNormal(v.normal);
//2.计算世界空间下的顶点坐标
fixed3 worldPos = mul(unity_ObjectToWorld,v.vertex).xyz;
/* //3.计算视角方向 内部是用摄像机位置-世界坐标位置
fixed3 worldViewDir = -UnityWorldSpaceViewDir(worldPos); */
/* //4.计算反射向量
o.worldRefl = reflect(worldViewDir,worldNormal); */
//将模型空间下的切线转换到世界空间下
float3 worldTangent = UnityObjectToWorldDir(v.tangent);
//计算世界空间下副切线
float3 worldBinormal = cross(normalize(worldTangent),normalize(worldNormal)) * v.tangent.w;
//变换矩阵的计算
o.TtoW0 = float4(worldTangent.x,worldBinormal.x,worldNormal.x,worldPos.x);
o.TtoW1 = float4(worldTangent.y,worldBinormal.y,worldNormal.y,worldPos.y);
o.TtoW2 = float4(worldTangent.z,worldBinormal.z,worldNormal.z,worldPos.z);

return o;
}
fixed4 frag(v2f i):SV_TARGET
{

//世界空间下的视角方向
float3 worldPos = float3(i.TtoW0.w,i.TtoW1.w,i.TtoW2.w);
fixed3 viewDir = normalize(UnityWorldSpaceViewDir(worldPos));
//通过纹理采样函数
//取出法线纹理贴图当中的数据
float4 packedNormal = tex2D(_BumpMap,i.uv.zw);
//将我们取出来的法线数据 进行逆运算并且可能会进行解压缩的运算 最终得到切线空间下的法线数据
float3 tangentNormal = UnpackNormal(packedNormal);
//把计算完成后的切线空间下的法线数据 转换到 世界空间下
//float3 worldNormal = mul(i.rotation,tangentNormal);
float3 worldNormal = float3(dot(i.TtoW0.xyz,tangentNormal),dot(i.TtoW1.xyz,tangentNormal),dot(i.TtoW2.xyz,tangentNormal));

//----反射颜色(叠加主纹理颜色)相关计算----
//对主纹理进行采样
fixed4 mainTex = tex2D(_MainTex,i.uv);

//计算反射向量,因为这时才把法线纹理中的法线信息计算出来
float3 refl = reflect(-viewDir,worldNormal);

//对立方体纹理利用相应的反射函数进行采样 并于主纹理进行叠加
fixed4 reflectColor = texCUBE(_Cube,refl) * mainTex;

//----折射颜色相关计算----
//其实就是从我们抓取的 屏幕渲染纹理中进行采样 参与计算
//抓取纹理中的颜色信息 相当于就是这个玻璃对象后面的颜色

//为了实现折射效果 我们需要将xy的屏幕坐标进行偏移 自定义规则
//这中中计算方式是图形学前辈们通过实践总结出来的 接近真实世界的折射效果
float2 offset = tangentNormal.xy * _Distortion;
i.grabPos.xy = offset * i.grabPos.z + i.grabPos.xy;//相当于偏移位置
fixed2 screenUV = i.grabPos.xy / i.grabPos.w;
//从捕获的屏幕纹理中采样 获取后面的颜色
fixed4 grabColor = tex2D(_GrabTexture,screenUV);

//float4 color = reflectColor*(1 - _RefreactAmount) + grabColor * _RefreactAmount;
float4 color = lerp(reflectColor,grabColor,_RefreactAmount);

return color;
}
ENDCG
}
}
}

评论