US3S3L7——双面渲染

双面渲染的透明效果的需求

对于现实世界的半透明物体,我们不仅可以透过它看到其他物体的样子,也可以看到这个物体自己的内部结构

但是我们之前实现的 透明度测试 和 透明度混合 相关Shader,都无法看到模型的内部结构
如下图,左边的立方体使用透明混合,右边的立方体使用透明测试,它们都无法看到模型内部的结构

image

而双面渲染的透明效果 Shader 就是来解决该问题的,
让我们不仅可以透过半透明物体看到其他物体的样子,还可以看到自己的内部结构

双面渲染的透明效果的基本原理

基本原理:默认情况下,Unity会自动剔除物体的背面,而只渲染物体的正面
双面渲染的基本原理就是利用我们之前学习过的 Cull​ 剔除指令来进行指定操作

  • Cull Back​ —— 背面剔除
  • Cull Front​ —— 正面剔除
  • Cull Off​ —— 不剔除

不设置的话,默认为背面剔除

  • 对于透明度测试 Shader,由于它无需混合,因此我们直接 关闭剔除 即可
  • 对于透明度混合 Shader,由于它需要进行混合,需要使用两个 Pass​,一个用于渲染背面,一个用于渲染正面

两个 Pass​ 中除了剔除命令不同,其他代码和之前一致

实现双面渲染的透明效果Shader

透明度测试

  1. 复用之前实现的 透明度测试 相关Shader代码
  2. Pass​ 中关闭剔除 Cull Off

实现的 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
Shader "TeachShader/Lesson59_AlphaTest"
{
Properties
{
_MainTex("MainTex", 2D) = ""{} // 纹理贴图
_MainColor("MainColor", Color) = (1, 1, 1, 1) // 漫反射颜色
_SpecularColor("SpecularColor", Color) = (1, 1, 1, 1) // 高光反射颜色
_SpecularNum("SpecularNum", Range(0, 20)) = 15 // 光泽度
_Cutoff("Curoff", Range(0, 1)) = 0 // 透明测试阈值
}
SubShader
{
Tags { "Queue"="AlphaTest" "IgnoreProjector"="True" "RenderType"="TransparentCutout"}

Pass
{
Tags { "LightMode" = "ForwardBase" }
Cull Off // 关闭剔除,让模型双面都可以渲染

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

// 贴图纹理对应的映射成员
sampler2D _MainTex;
float4 _MainTex_ST;
// 漫反射颜色、高光反射颜色、光泽度
fixed4 _MainColor;
fixed4 _SpecularColor;
float _SpecularNum;
// 透明测试阈值
fixed _Cutoff;

struct v2f
{
float4 pos: SV_POSITION; // 裁剪空间下的顶点坐标
float2 uv: TEXCOORD0; // 纹理UV坐标
float3 wNormal: NORMAL; // 世界空间下的法线
float3 wPos: TEXCOORD1; // 世界空间下的顶点坐标
};

v2f vert (appdata_base v)
{
v2f data;
data.pos = UnityObjectToClipPos(v.vertex); // 将模型空间下的法线转换到世界空间下
data.uv = v.texcoord.xy * _MainTex_ST.xy + _MainTex_ST.zw; // 计算UV
data.wNormal = UnityObjectToWorldNormal(v.normal); // 法线转换到世界空间
data.wPos = mul(unity_ObjectToWorld, v.vertex); // 顶点转换到世界空间

return data;
}

fixed4 frag (v2f i) : SV_Target
{
fixed4 texColor = tex2D(_MainTex, i.uv); // 颜色纹理的颜色信息
clip(texColor.a - _Cutoff); // A通道减去阈值传入到Clip函数内,若A值小于阈值就会被裁剪

fixed3 albedo = texColor.rgb * _MainColor.rgb; // 反射率,即纹理颜色和漫反射材质颜色乘法叠加共同决定的颜色

// 漫反射颜色
float3 lightDir = normalize(_WorldSpaceLightPos0.xyz); // 指向光源的方向
fixed3 lambertColor = _LightColor0.rgb * albedo.rgb * max(0, dot(i.wNormal, lightDir));

// 高光反射颜色
float3 viewDir = normalize(UnityWorldSpaceViewDir(i.wPos)); // 视角方向
float3 halfA = normalize(viewDir + lightDir); // 半角向量
fixed3 specularColor = _LightColor0.rgb * _SpecularColor.rgb * pow(max(0, dot(i.wNormal, halfA)), _SpecularNum);

// 最终颜色 = 环境光 * 反射颜色 + 漫反射颜色 + 高光反射颜色
fixed3 color = UNITY_LIGHTMODEL_AMBIENT.rgb * albedo + lambertColor + specularColor;

return fixed4(color.rgb, 1);
}
ENDCG
}
}
}

透明度混合

  1. 复用之前实现的 透明度混合 相关Shader代码

  2. 复制之前的 Pass​,变成两个一模一样的 Pass

  3. 在第一个 Pass​ 中剔除正面 Cull Front​,在第二个 Pass​ 中剔除背面 Cull Back

    相当于一个片元先渲染背面再渲染正面

实现的 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
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
138
139
140
141
142
143
144
145
Shader "TeachShader/Lesson59_Transparent"
{
Properties
{
_MainTex("MainTex", 2D) = ""{} // 纹理贴图
_MainColor("MainColor", Color) = (1, 1, 1, 1) // 漫反射颜色
_SpecularColor("SpecularColor", Color) = (1, 1, 1, 1) // 高光反射颜色
_SpecularNum("SpecularNum", Range(0, 20)) = 15 // 光泽度
_AlphaScale("AlphaScale", Range(0, 1)) = 1 // 对象总体透明度
}
SubShader
{
Tags { "Queue"="Transparent" "IgnoreProjector"="True" "RenderType"="Transparent" }

// 渲染背面的Pass
Pass
{
Tags { "LightMode" = "ForwardBase" }
ZWrite Off //半透明效果关闭深度写入
Blend SrcAlpha OneMinusSrcAlpha //设置混合因子
Cull Front //剔除正面,先渲染背面

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

// 贴图纹理对应的映射成员
sampler2D _MainTex;
float4 _MainTex_ST;
// 漫反射颜色、高光反射颜色、光泽度
fixed4 _MainColor;
fixed4 _SpecularColor;
float _SpecularNum;
// 对象整体透明度
fixed _AlphaScale;

struct v2f
{
float4 pos: SV_POSITION; // 裁剪空间下的顶点坐标
float2 uv: TEXCOORD0; // 纹理UV坐标
float3 wNormal: NORMAL; // 世界空间下的法线
float3 wPos: TEXCOORD1; // 世界空间下的顶点坐标
};

v2f vert (appdata_base v)
{
v2f data;
data.pos = UnityObjectToClipPos(v.vertex); // 将模型空间下的法线转换到世界空间下
data.uv = v.texcoord.xy * _MainTex_ST.xy + _MainTex_ST.zw; // 计算UV
data.wNormal = UnityObjectToWorldNormal(v.normal); // 法线转换到世界空间
data.wPos = mul(unity_ObjectToWorld, v.vertex); // 顶点转换到世界空间

return data;
}

fixed4 frag (v2f i) : SV_Target
{
fixed4 texColor = tex2D(_MainTex, i.uv); // 取出纹理的颜色
fixed3 albedo = texColor.rgb * _MainColor.rgb; // 反射率,即纹理颜色和漫反射材质颜色乘法叠加共同决定的颜色

// 漫反射颜色
float3 lightDir = normalize(_WorldSpaceLightPos0.xyz); // 指向光源的方向
fixed3 lambertColor = _LightColor0.rgb * albedo.rgb * max(0, dot(i.wNormal, lightDir));

// 高光反射颜色
float3 viewDir = normalize(UnityWorldSpaceViewDir(i.wPos)); // 视角方向
float3 halfA = normalize(viewDir + lightDir); // 半角向量
fixed3 specularColor = _LightColor0.rgb * _SpecularColor.rgb * pow(max(0, dot(i.wNormal, halfA)), _SpecularNum);

// 最终颜色 = 环境光 * 反射颜色 + 漫反射颜色 + 高光反射颜色
fixed3 color = UNITY_LIGHTMODEL_AMBIENT.rgb * albedo + lambertColor + specularColor;
// 最终透明度 = A通道值 * 整体透明度
return fixed4(color.rgb, texColor.a * _AlphaScale);
}
ENDCG
}

// 渲染正面的Pass
Pass
{
Tags { "LightMode" = "ForwardBase" }
ZWrite Off //半透明效果关闭深度写入
Blend SrcAlpha OneMinusSrcAlpha //设置混合因子
Cull Back //剔除背面,渲染正面

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

// 贴图纹理对应的映射成员
sampler2D _MainTex;
float4 _MainTex_ST;
// 漫反射颜色、高光反射颜色、光泽度
fixed4 _MainColor;
fixed4 _SpecularColor;
float _SpecularNum;
// 对象整体透明度
fixed _AlphaScale;

struct v2f
{
float4 pos: SV_POSITION; // 裁剪空间下的顶点坐标
float2 uv: TEXCOORD0; // 纹理UV坐标
float3 wNormal: NORMAL; // 世界空间下的法线
float3 wPos: TEXCOORD1; // 世界空间下的顶点坐标
};

v2f vert (appdata_base v)
{
v2f data;
data.pos = UnityObjectToClipPos(v.vertex); // 将模型空间下的法线转换到世界空间下
data.uv = v.texcoord.xy * _MainTex_ST.xy + _MainTex_ST.zw; // 计算UV
data.wNormal = UnityObjectToWorldNormal(v.normal); // 法线转换到世界空间
data.wPos = mul(unity_ObjectToWorld, v.vertex); // 顶点转换到世界空间

return data;
}

fixed4 frag (v2f i) : SV_Target
{
fixed4 texColor = tex2D(_MainTex, i.uv); // 取出纹理的颜色
fixed3 albedo = texColor.rgb * _MainColor.rgb; // 反射率,即纹理颜色和漫反射材质颜色乘法叠加共同决定的颜色

// 漫反射颜色
float3 lightDir = normalize(_WorldSpaceLightPos0.xyz); // 指向光源的方向
fixed3 lambertColor = _LightColor0.rgb * albedo.rgb * max(0, dot(i.wNormal, lightDir));

// 高光反射颜色
float3 viewDir = normalize(UnityWorldSpaceViewDir(i.wPos)); // 视角方向
float3 halfA = normalize(viewDir + lightDir); // 半角向量
fixed3 specularColor = _LightColor0.rgb * _SpecularColor.rgb * pow(max(0, dot(i.wNormal, halfA)), _SpecularNum);

// 最终颜色 = 环境光 * 反射颜色 + 漫反射颜色 + 高光反射颜色
fixed3 color = UNITY_LIGHTMODEL_AMBIENT.rgb * albedo + lambertColor + specularColor;
// 最终透明度 = A通道值 * 整体透明度
return fixed4(color.rgb, texColor.a * _AlphaScale);
}
ENDCG
}
}
}

显示效果如下(下方是不使用双面渲染的混合和透明测试,上方是使用双面渲染的混合和透明测试):

image