Unity-Shader读书笔记(9)

高级纹理

立方体纹理(CubeMap)

        在图形学中,立方体纹理是环境映射(Environment Mapping)的一种实现方法。环境映射可以模拟物体周围的环境,而使用了环境映射的物体看起来像镀了层金属一样反射出周围的环境。

        立方体纹理一共包含了6张图像,这些图像对应着立方体的6个面。在之前的普通纹理中,我们使用uv坐标进行采样。而在立方体纹理中,我们使用一个三维的向量值进行采样。这个方向矢量的起点位于立方体纹理的中心,然后按照矢量的方向延伸。最终这个方向向量会在立方体纹理6个面之间相交,而采样值就是通过交点计算出来的。

        立方体纹理的优点在于它实现简单快速,而且得到的效果也好。但它同时也有缺点,如果场景中引入了新的物体、光源或者物体发生移动时,我们就要重新生成立方体纹理。立方体纹理不能反射使用该立方体纹理的物体,也就是说立方体纹理不能进行多次反射。、

        立方体纹理在实时渲染中有很多的应用,最常见的就是用于天空盒(SkyBox)以及环境映射。

天空盒

        天空盒是游戏中模拟背景的一种方式。在Unity中使用天空盒,我们只需要创建一个材质然后材质的Shader选择为Unity自带的Skybox/6 Sided。6张纹理用资源中的Assets6张图片,接下来我们就只要按照名称一一对应就好了(posx对应的就是Left[+x])。并且我们要将6张纹理的Wrap Mode设置为Clamp防止在接缝处不正确。Unity自带的Shader中还有两个属性Exposure和Rotation,Exposure用于调整天空盒的亮度,Rotation用于调整天空盒沿+y轴方向的旋转角度。最后我们将调整好的天空盒材质放置到设置中。不同版本的设置地方不同,比如书中说的是Windows下的Lighting选项,而我的2019.4.40f1版本则是Windows下的Rendering中的Lighting。大家可以根据自己的版本去Unity官网中搜索,Lighting 窗口 - Unity 手册。而主摄像机要显示出天空盒,那么它必须将它的Clear Flags属性设置为Skybox。

用于环境映射的立方体纹理

       书中虽然说了三种方法,但是我这里就说明其中一种,因为书中也只实现了一种。我们可以使用脚本来创建立方体纹理,这样做的好处就在于我们可以根据物体在场景不同的位置来生成不同的纹理。我们需要用到Camera.RenderToCubemap函数来实现。具体代码如下:

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
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEditor;

namespace EditorTool
{
public class CubeMapRender : ScriptableWizard
{
public Transform renderFromPosition;
public Cubemap cubemap;

void OnWizardUpdate()
{
helpString = "Select transform to render from and cubemap to render into";
isValid = (renderFromPosition != null) && (cubemap != null);
}

void OnWizardCreate()
{
// create temporary camera for rendering
GameObject go = new GameObject("CubemapCamera");
go.AddComponent<Camera>();
// place it on the object
go.transform.position = renderFromPosition.position;
// render into cubemap
go.GetComponent<Camera>().RenderToCubemap(cubemap);

// destroy temporary camera
DestroyImmediate(go);
}

[MenuItem("GameObject/Render into Cubemap")]
static void RenderCubemap()
{
ScriptableWizard.DisplayWizard<CubeMapRender>(
"Render cubemap", "Render!");
}
}
}

因为这个代码要添加到菜单栏中,所以大家要将其放置在Assets的Editor目录下。如果没有Editor文件,那么你要就要自己创建一个。只有在这个特殊的目录下,添加到菜单栏的代码才会生效。接下来,我们就只要在之前的设定的场景中加入一个空物体,然后在文件中右键选择Create→Legacy→cubemap创建出立方体纹理。大家要记得将这个立方体纹理设定为Readable,否则执行代码的时候就报下面的错误:Unable to render to cubemap for camera with name 'CubemapCamera'. Make sure it's marked as 'Readable'。接下来我们将主摄像机拉到Render From Position中,然后将我们创建的cubemap拉到Cube Map中。最后我们点击渲染按钮就可以得到立方体纹理。

环境映射

        环境映射最常见的应用就是反射和折射。

反射

        我们使用立方体纹理模拟反射效果,我们只需要通过入射的方向和表面法线方向计算反射,然后我们用这个反射方向去做采样。具体代码如下:

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
Shader "Learn/CubeMapReflectShader"
{
Properties
{
_DiffuseColor ("DiffuseColor", Color) = (1,1,1,1)
_SpecularColor ("SpecularColor",Color) = (1,1,1,1)
// 高光系数
_Gloss ("Gloss",Range(8,255)) = 20
_ReflectColor ("Reflection Color", Color) = (1,1,1,1)
_ReflectAmount ("Reflect Amount",Range(0,1)) = 1
_Cubemap ("Reflection Cubemap", Cube) = "_Skybox"{}
}
SubShader
{
Tags { "RenderType"="Opaque"}

Pass
{
// 前向渲染的base pass
Tags { "LightMode"="ForwardBase" }

CGPROGRAM
// 使用这个指令,我们就可以保证Shader中使用的光照变量可以被正确赋值。
#pragma multi_compile_fwdbase
#pragma vertex vert
#pragma fragment frag

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

struct a2v
{
float4 vertex : POSITION;
float3 normal : NORMAL;
};

struct v2f
{
float4 pos : SV_POSITION;
float3 worldPos : TEXCOORD1;
float3 worldNormal : TEXCOORD0;
SHADOW_COORDS(2)
};

float4 _DiffuseColor;
float4 _SpecularColor;
fixed _Gloss;
float4 _ReflectColor;
fixed _ReflectAmount;
samplerCUBE _Cubemap;

v2f vert (a2v v)
{
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
//o.worldPos = mul(v.vertex,(float3x3)unity_WorldToObject);
o.worldPos = mul(unity_ObjectToWorld,v.vertex);
// 获取法线处于世界空间下的值,我们仍然只要得到单位向量
fixed3 worldNormal = normalize(mul(v.normal,(float3x3)unity_WorldToObject));
// 返回的颜色是漫反射加上环境光
o.worldNormal = worldNormal;
TRANSFER_SHADOW(o);
return o;
}

fixed4 frag (v2f i) : SV_Target
{
// 获取到环境光
fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz;
// 获取世界坐标系下灯光的单位向量
fixed3 worldLight = normalize(UnityWorldSpaceLightDir(i.worldPos));

i.worldNormal = normalize(i.worldNormal);
// 得到反射光
fixed3 reflectDir = reflect(-worldLight,i.worldNormal);
reflectDir = normalize(reflectDir);
// 观察方向
fixed3 viewDir = normalize(UnityWorldSpaceViewDir(i.worldPos));
// 得到CubeMap的反射光
fixed3 reflectCubeDir = reflect(-viewDir,i.worldNormal);
fixed3 reflectCube = texCUBE(_Cubemap,reflectCubeDir).rgb * _ReflectColor.rgb;
// 计算得到漫反射值
fixed3 diffuse = _LightColor0.rgb * _DiffuseColor.rgb * saturate(dot(i.worldNormal,worldLight));
fixed3 specular = _LightColor0.rgb * _SpecularColor.rgb * pow(saturate(dot(viewDir,reflectDir)),_Gloss);
fixed3 color = lerp(diffuse,reflectCube,_ReflectAmount) + specular;
UNITY_LIGHT_ATTENUATION(atten,i,i.worldPos);
color *= atten;
color += ambient;
return fixed4(color,1);
}
ENDCG
}
}
}

        我们还是用前向渲染阴影的Shader作为原型。然后我们在属性中加入了_ReflectColor控制反射颜色,_ReflectAmount控制反射的程度。我们要直接得到哪个光的方向进入我们眼睛有些困难。所以我们利用光线可逆这个原理,我们将视线方向的反方向作为入射光计算出我们要得到的光的方向。然后我们使用这个光的方向对CubeMap做纹理采样。最后我们根据我们设置的反射程度让漫反射值和采集值做插值来混合。显然当反射程度为1的时候,那就是全反射了(大家可以想象一下镜子的反射)。

折射

        我们要实现折射就要使用斯涅尔定律,关于这个定律百度上的解释为斯涅耳定律_百度百科。我们如果要使用这个定律则要知道4个变量,两个介质间的折射率和他们的角度。一般而言,当得到折射方向后我们就会直接使用这个来对立方体采样,但是这个不符合物理规律。对一个透明物体来说,一种更准确的模拟方法需要计算两次折射一次是光线进入它内部,一次是光线从它内部出来。但是这个实现起来有些困难,所以我们通常只模拟一次。幸好这样效果也过得去。

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
Shader "Learn/RefractionShader"
{
Properties
{
_DiffuseColor ("DiffuseColor", Color) = (1,1,1,1)
_SpecularColor ("SpecularColor",Color) = (1,1,1,1)
// 高光系数
_Gloss ("Gloss",Range(8,255)) = 20
_RefractionColor ("Refraction Color", Color) = (1,1,1,1)
_RefractionAmount ("Refraction Amount",Range(0,1)) = 1
// 透射比
_RefractionRatio ("Refraction Amount",Range(0.1,1)) = 0.5
_Cubemap ("Reflection Cubemap", Cube) = "_Skybox"{}
}
SubShader
{
Tags { "RenderType"="Opaque" }

Pass
{
// 前向渲染的base pass
Tags { "LightMode"="ForwardBase" }

CGPROGRAM
// 使用这个指令,我们就可以保证Shader中使用的光照变量可以被正确赋值。
#pragma multi_compile_fwdbase
#pragma vertex vert
#pragma fragment frag

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

struct a2v
{
float4 vertex : POSITION;
float3 normal : NORMAL;
};

struct v2f
{
float4 pos : SV_POSITION;
float3 worldPos : TEXCOORD1;
float3 worldNormal : TEXCOORD0;
SHADOW_COORDS(2)
};

float4 _DiffuseColor;
float4 _SpecularColor;
fixed _Gloss;
float4 _RefractionColor;
fixed _RefractionAmount;
fixed _RefractionRatio;
samplerCUBE _Cubemap;

v2f vert (a2v v)
{
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
//o.worldPos = mul(v.vertex,(float3x3)unity_WorldToObject);
o.worldPos = mul(unity_ObjectToWorld,v.vertex);
// 获取法线处于世界空间下的值,我们仍然只要得到单位向量
fixed3 worldNormal = normalize(mul(v.normal,(float3x3)unity_WorldToObject));
// 返回的颜色是漫反射加上环境光
o.worldNormal = worldNormal;
TRANSFER_SHADOW(o);
return o;
}

fixed4 frag (v2f i) : SV_Target
{
// 获取到环境光
fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz;
// 获取世界坐标系下灯光的单位向量
fixed3 worldLight = normalize(UnityWorldSpaceLightDir(i.worldPos));

i.worldNormal = normalize(i.worldNormal);
// 得到反射光
fixed3 reflectDir = reflect(-worldLight,i.worldNormal);
reflectDir = normalize(reflectDir);
// 观察方向
fixed3 viewDir = normalize(UnityWorldSpaceViewDir(i.worldPos));
// 得到CubeMap的折射光
fixed3 refractionCubeDir = refract(-viewDir,i.worldNormal,_RefractionRatio);
fixed3 refractionCube = texCUBE(_Cubemap,refractionCubeDir).rgb * _RefractionColor.rgb;
// 计算得到漫反射值
fixed3 diffuse = _LightColor0.rgb * _DiffuseColor.rgb * saturate(dot(i.worldNormal,worldLight));
fixed3 specular = _LightColor0.rgb * _SpecularColor.rgb * pow(saturate(dot(viewDir,reflectDir)),_Gloss);
fixed3 color = lerp(diffuse,refractionCube,_RefractionAmount) + specular;
UNITY_LIGHT_ATTENUATION(atten,i,i.worldPos);
color *= atten;
color += ambient;
return fixed4(color,1);
}
ENDCG
}
}
}

_RefractionColor和_RefractionAmount与之前的反射中的_ReflectColor和_ReflectAmount是一样的效果。不同的是我们新加入了一个透射比_RefractionRatio,关于透射比百度百科链接:透射比_百度百科。只有我们设定了透射比我们才可以使用Unity封装好的折射函数refract,然后我们再利用光线可逆原理来得到我们想要光的方向。剩下的操作就和反射中的操作一样了。

菲涅耳反射

        关于菲涅耳反射,我这边直接贴百度百科:菲涅尔反射_百度百科。我这边找了书中提到的文章:中文英文。这里我们实现方法是使用一个近似公式得出,这是因为真实的菲涅耳反射十分复杂。

        其中有一个著名的近似公式就是Schlick菲涅耳近似公式:F0+(1-F0)(1-dot(v,n))^5。其中,F0是一个反射系数用于控制菲涅耳反射的强度,v是视角方向,n是表面法线。

        另一个应用比较广泛的等式是Empricial菲涅耳近似公式:max(0,min(1,bias+scale*(1-dot(v,n)^power)))。其中bias、scale和power是控制项。

        下面的Shader代码是使用Schlick菲涅耳近似公式来实现的。

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
Shader "Learn/FresnelShader"
{
Properties
{
_DiffuseColor ("DiffuseColor", Color) = (1,1,1,1)
_SpecularColor ("SpecularColor",Color) = (1,1,1,1)
// 高光系数
_Gloss ("Gloss",Range(8,255)) = 20
_FresnelScale("Fresnel Scale",Range(0,1)) = 0.5
_Cubemap ("Reflection Cubemap", Cube) = "_Skybox"{}
}
SubShader
{
Tags { "RenderType"="Opaque" }

Pass
{
// 前向渲染的base pass
Tags { "LightMode"="ForwardBase" }

CGPROGRAM
// 使用这个指令,我们就可以保证Shader中使用的光照变量可以被正确赋值。
#pragma multi_compile_fwdbase
#pragma vertex vert
#pragma fragment frag

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

struct a2v
{
float4 vertex : POSITION;
float3 normal : NORMAL;
};

struct v2f
{
float4 pos : SV_POSITION;
float3 worldPos : TEXCOORD1;
float3 worldNormal : TEXCOORD0;
SHADOW_COORDS(2)
};

float4 _DiffuseColor;
float4 _SpecularColor;
fixed _Gloss;
fixed _FresnelScale;
samplerCUBE _Cubemap;

v2f vert (a2v v)
{
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
//o.worldPos = mul(v.vertex,(float3x3)unity_WorldToObject);
o.worldPos = mul(unity_ObjectToWorld,v.vertex);
// 获取法线处于世界空间下的值,我们仍然只要得到单位向量
fixed3 worldNormal = normalize(mul(v.normal,(float3x3)unity_WorldToObject));
// 返回的颜色是漫反射加上环境光
o.worldNormal = worldNormal;
TRANSFER_SHADOW(o);
return o;
}

fixed4 frag (v2f i) : SV_Target
{
// 获取到环境光
fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz;
// 获取世界坐标系下灯光的单位向量
fixed3 worldLight = normalize(UnityWorldSpaceLightDir(i.worldPos));

i.worldNormal = normalize(i.worldNormal);
// 得到反射光
fixed3 reflectDir = reflect(-worldLight,i.worldNormal);
reflectDir = normalize(reflectDir);
// 观察方向
fixed3 viewDir = normalize(UnityWorldSpaceViewDir(i.worldPos));
// 得到CubeMap的反射光
fixed3 reflectCubeDir = reflect(-viewDir,i.worldNormal);
fixed3 reflectCube = texCUBE(_Cubemap,reflectCubeDir).rgb;
// 得到菲涅耳反射系数值
fixed fresnel = saturate(_FresnelScale + (1 - _FresnelScale)*(1 - pow(dot(viewDir,i.worldNormal),5)));
// 计算得到漫反射值
fixed3 diffuse = _LightColor0.rgb * _DiffuseColor.rgb * saturate(dot(i.worldNormal,worldLight));
fixed3 specular = _LightColor0.rgb * _SpecularColor.rgb * pow(saturate(dot(viewDir,reflectDir)),_Gloss);
fixed3 color = lerp(diffuse,reflectCube,fresnel) + specular;
UNITY_LIGHT_ATTENUATION(atten,i,i.worldPos);
color *= atten;
color += ambient;
return fixed4(color,1);
}
ENDCG
}
}
}

        我们使用公式得到的值来混合漫反射和反射光。一些实现中会将得到的值和反射光线相乘后叠加到漫反射光照上,模拟边缘光照的效果。

渲染纹理

        一个摄像机的渲染结果会输出到颜色缓冲中,并显示到我们的屏幕上。现在的GPU支持我们把整个三维场景渲染到一个中间缓冲中,即渲染目标纹理(Render Target Texture,RTT),而不是传统的帧缓冲或者后备缓冲(back buffer)。与之相关的是多重渲染目标(Multiple Render Target,MRT),这种技术指的是GPU允许我们把场景同时渲染到多个目标纹理中,而不再需要为每一个渲染目标纹理单独渲染完整的场景。延迟渲染就是使用多重渲染目标的一个应用。

        Unity为渲染目标纹理定义了一个专门的纹理类型——渲染纹理(Render Texture)。

镜子效果

PS:最好直接使用项目文件中的场景scene_10_2_1。如果有丢失的材质直接新建一个标准材质就好了,然后按照你要的颜色修改一下。然后我们将新创建的Render Texture赋值给Mirror摄像机的Target Texture。之所以不描述一下摆放,就是因为这个很麻烦没必要。

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
Shader "Learn/MirrorUseRenderTextureShader"
{
Properties
{
_MainTex ("Texture", 2D) = "white" {}
}
SubShader
{
Tags { "RenderType"="Opaque" }
LOD 100

Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag

#include "UnityCG.cginc"

struct appdata
{
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
};

struct v2f
{
float2 uv : TEXCOORD0;
float4 vertex : SV_POSITION;
};

sampler2D _MainTex;
float4 _MainTex_ST;

v2f vert (appdata v)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
o.uv = v.uv;
// 因为要实现镜子效果需要翻转
o.uv.x = 1 - o.uv.x;
return o;
}

fixed4 frag (v2f i) : SV_Target
{
return tex2D(_MainTex, i.uv);
}
ENDCG
}
}
}

        用这个方法实现基本没什么坑点,说到底还是对纹理进行采样。只是镜面要进行x轴的翻转这个是要注意一下的。还有一点是我们创建是Render Texture是可以自己设定大小,默认创建出来就是256*256,所以如果你近一点看效果你就会发现上面锯齿十分严重。如果你要更好的效果那么就提高分辨率,只是更高的分辨率会影响带宽和性能。

玻璃效果

        在Unity中,我们还可以在Unity Shader中使用GrabPass来完成获取屏幕图像的目的。当我们在Shader中定义了一个GrabPass后,Unity就会把当前屏幕的图像绘制在一张纹理中,以便我们在后续的Pass中访问它。GrabPass通常是用来实现诸如玻璃等透明材质的模拟,与使用简单的透明度混合不同,我们使用GrabPass可以让我们对物体后面的图像进行更加复杂的处理。

PS:最好直接使用项目文件中的场景scene_10_2_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
Shader "Learn/GlassShader" {
Properties
{
_MainTex ("Texture", 2D) = "white" {}
_NormalTex("Normal Texture",2D) = "bump"{}
_CubeMap("Environment CubeMap",Cube) = "Skybox"{}
_Distortion("Distortion",Range(0,100)) = 10
_RefractionAmount("Refraction Amount",Range(0,1)) = 1.0
}
SubShader {
Tags { "Queue"="Transparent" "RenderType"="Opaque" }

GrabPass{"_RefractionTex"}
LOD 100

Pass {
CGPROGRAM

#pragma vertex vert
#pragma fragment frag

#include "UnityCG.cginc"

sampler2D _MainTex;
float4 _MainTex_ST;
sampler2D _NormalTex;
float4 _NormalTex_ST;
samplerCUBE _CubeMap;
float _Distortion;
fixed _RefractionAmount;
sampler2D _RefractionTex;
float4 _RefractionTex_TexelSize;

struct a2v {
float4 vertex : POSITION;
float3 normal : NORMAL;
float4 texcoord : TEXCOORD0;
float4 tangent : TANGENT;
};

struct v2f {
float4 uv : TEXCOORD0;
float4 pos : SV_POSITION;
float4 T2W0 : TEXCOORD1;
float4 T2W1 : TEXCOORD2;
float4 T2W2 : TEXCOORD3;
float4 scrPos : TEXCOORD4;
};

v2f vert (a2v v) {
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
o.scrPos = ComputeGrabScreenPos(o.pos);
o.uv.xy = TRANSFORM_TEX(v.texcoord, _MainTex);
o.uv.zw = TRANSFORM_TEX(v.texcoord, _NormalTex);
float3 worldNormal = mul(v.normal,(float3x3)unity_WorldToObject);
float3 worldPos = mul(unity_ObjectToWorld,v.vertex);
float3 worldTangent = UnityObjectToWorldDir(v.tangent.xyz);
float3 worldBinormal = cross(normalize(worldNormal),normalize(worldTangent)) * v.tangent.w;
// 得到变换矩阵,并将世界坐标存储其中
o.T2W0 = float4(worldTangent.x,worldBinormal.x,worldNormal.x,worldPos.x);
o.T2W1 = float4(worldTangent.y,worldBinormal.y,worldNormal.x,worldPos.y);
o.T2W2 = float4(worldTangent.z,worldBinormal.z,worldNormal.z,worldPos.z);
return o;
}

fixed4 frag (v2f i) : SV_Target {
// 从信息中取出我们想要的值
float3 worldPos = float3(i.T2W0.w,i.T2W1.w,i.T2W2.w);
// 得到在切线空间下的法线值
fixed3 bump = UnpackNormal(tex2D(_NormalTex,i.uv.zw));
bump.z = sqrt(1 - saturate(dot(bump.xy,bump.xy)));
float2 offset = bump.xy * _Distortion * _RefractionTex_TexelSize.xy;
i.scrPos.xy = offset * i.scrPos.z + i.scrPos.xy;
fixed3 refrColor = tex2D(_RefractionTex, i.scrPos.xy/i.scrPos.w).rgb;

// 将其转变到世界空间下,代替之前的法线值
bump = normalize(float3(dot(i.T2W0.xyz,bump),dot(i.T2W1.xyz,bump),dot(i.T2W2.xyz,bump)));
// 观察方向
fixed3 viewDir = normalize(UnityWorldSpaceViewDir(worldPos));
fixed3 reflectCubeDir = reflect(-viewDir,bump);
fixed3 reflColor = tex2D(_MainTex,i.uv.xy).rgb * texCUBE(_CubeMap,reflectCubeDir).rgb;

fixed3 color = reflColor * (1 - _RefractionAmount) + refrColor * _RefractionAmount;
return fixed4(color,1);
}

ENDCG
}
}

FallBack "Diffuse"
}

        属性中我们使用_Distortion来模拟折射的扭曲度,_RefractionAmount控制折射的程度。当_RefractionAmount为1的时候那就只有反射,而当_RefractionAmount为0的时候就只有折射了。

        shader设定Queue为Transparent,但是RenderType设定为Opaque。这样看似矛盾,但实际上服务于不同的需求。Queue是为了等待所有不透明的物体都已经渲染完毕了,否则我们得不到透过玻璃看到的图像。而设置RenderType是为了在使用着色器替换的时候,该物体可以被正确的渲染。这通常发生在我们需要摄像机的深度和法线纹理的时候。

        随后我们调用GrabPass{"_RefractionTex"},其将得到的屏幕纹理赋值到_RefractionTex这个纹理中。这也意味着我们要多设定sampler2D _RefractionTex。与之前不同的是我们并没有用float4 _RefractionTex_ST而是用了float4 _RefractionTex_TexelSize。_RefractionTex_TexelSize可以让我们得到该纹理的纹素大小,例如一个大小为256*512的纹理,它的纹素大小就是(1/256,1/512)。我们需要在对屏幕图像的采样坐标进行偏移时使用该变量。

        因为我们使用了屏幕纹理,所以我们在片元着色器结构体中设定了一个scrPos来存储模型该点对应的屏幕坐标。并且我们使用ComputeGrabScreenPos来得到屏幕坐标值。在片元函数中,我们还对这个屏幕坐标做了偏移。我们使用切线空间下的法线方向进行偏移,是因为该空间下的法线可以反映顶点局部空间下的法线方向。并且_Distortion来模拟折射,其值越大,偏移量越大。随后我们对scrPos进行透视除法得到真正的屏幕坐标,再使用该坐标对抓取的屏幕图像_RefractionTex进行采样得到模拟颜色。

        我们使用世界空间下的法线进行采样得到反射值。最后再将反射值的颜色和折射值的颜色进行混合。

渲染纹理和GrabPass的对比

        GrabPass比较简单,它不需要像渲染纹理一样做那么复杂的操作。但是渲染纹理有着更好的性能。