0%

【Unity Shader】(三)透明效果

透明是游戏中经常要使用的一种效果。这一节我们学习透明效果是如何实现的。

1 透明效果的原理

在实时渲染中要实现透明效果,通常会在渲染模型时控制它的透明通道(Alpha Channel)。当开启透明混合后,当一个物体被渲染到屏幕上时,每个片元除了颜色值和深度值之外,它还有另一个属性——透明度。当透明度为 1 时,表示该像素是完全不透明的,而当其为 0 时,则表示该像素完全不会显示。

Unity 中,我们通常使用两种方法来实现透明效果:第一种是使用透明度测试(Alpha Test),这种方法其实无法得到真正的半透明效果;另一种是透明度混合(Alpha Blending)

1.1 透明度测试

透明度测试的原理是:只要一个片元的透明度不满足条件(通常是小于某个阈值),那么它对应的片元就会被舍弃。被舍弃的片元将不会再进行任何处理,也不会对颜色缓冲产生任何影响;否则,就会按照普通的不透明物体的处理方式来处理它,即进行深度测试、深度写入等。它和其他不透明物体最大的不同就是它会根据透明度来舍弃一些片元。虽然简单,但是它产生的效果也很极端,要么完全透明,即看不到,要么完全不透明,就像不透明物体那样。

1.2 透明度混合

在之前的学习中,我们从没有强调过渲染顺序的问题。也就是说,当场景中包含很多模型时,我们并没有考虑是先渲染 A,再渲染 B,最后再渲染 C,还是按照其他的顺序来渲染。事实上,对于不透明 (opaque) 物体,不考虑它们的渲染顺序也能得到正确的排序效果,这是由于强大的深度缓冲 z-buffer 的存在。但如果想要实现透明效果,事情就不那么简单了,这是因为, 当使用透明度混合时,我们关闭了深度写入 (ZWrite) 。

透明度混合可以得到真正的半透明效果。它会使用当前片元的透明度作为混合因子,与已经存储在颜色缓冲中的颜色值进行混合,得到新的颜色。但是,透明度混合需要关闭深度写入,这使得我们要非常小心物体的渲染顺序。需要注意的是,透明度混合只关闭了深度写入,但没有关闭深度测试。这意味着,当使用透明度混合渲染一个片元时,还是会比较它的深度值与当前深度缓冲中的深度值,如果它的深度值距离摄像机更远,那么就不会再进行混合操作。这一点决定了,当一个不透明物体出现在一个透明物体的前面,而我们先渲染了不透明物体,它仍然可以正常地遮挡住透明物体。也就是说,对于透明度混合来说,深度缓冲是只读的。

1.3 渲染顺序

那么我们为什么要关闭深度写入呢?如果不关闭深度写入,一个半透明表面背后的表面本来是可以透过它被我们看到的,但由于深度测试时判断结果是该半透明表面距离摄像机更近,导致后面的表面将会被剔除,我们也就无法透过半透明表面看到后面的物体了。但是,我们由此就破坏了深度缓冲的工作机制,而这是一个非常非常非常(重要的事情要讲3遍)糟糕的事情, 尽管我们不得不这样做。关闭深度写入导致渲染顺序将变得非常重要。

我们来考虑最简单的情况。假设场景里有两个物体A和B,如下图:

image-20220403171332205

其中 A 是半透明物体,而 B 是不透明物体。我们来考虑不同的渲染顺序会有什么结果:

  • 第一种情况,我们先渲染 B,再渲染 A。那么由于不透明物体开启了深度测试和深度写入,而此时深度缓冲中没有任何有效数据,因此 B 首先会写入颜色缓冲和深度缓冲。随后我们渲染 A,透明物体仍然会进行深度测试,因此我们发现和 B 相比 A 距离摄像机更近,因此,我们会使用 A 的透明度来和颜色缓冲中的 B 的颜色进行混合,得到正确的半透明效果;
  • 第二种情况,我们先渲染 A,再渲染 B。渲染 A 时,深度缓冲区中没有任何有效数据,因此 A 直接写入颜色缓冲,但由于对半透明物体关闭了深度写入,因此 A 不会修改深度缓冲。等到渲染 B 时,B 会进行深度测试,它发现,”咦,深度缓存中还没有人来过,那我就放心地写入颜色缓冲了!“ 结果就是 B 会直接覆盖 A 的颜色。从视觉上来看,B 就出现在了 A 的前面,而这是错误的。、

从这个例子可以看出, 当关闭了深度写入后, 渲染顺序是多么重要。 由此我们知道, 我们应该在不透明物体渲染完之后再渲染半透明物体。那么,如果都是半透明物体,渲染顺序还重要吗?答案是肯定的。还是假设场景里有两个物体 A 和 B ,如下图,其中 A 和 B 都是半透明物体。

image-20220403171748283

我们还是考虑不同的渲染顺序有什么不同结果:

  • 第一种情况,我们先渲染 B,再渲染 A。那么 B 会正常写入颜色缓冲,然后 A 会和颜色缓冲中的 B 颜色进行混合,得到正确的半透明效果。
  • 第二种情况,我们先渲染 A,再渲染 B。那么 A 会先写入颜色缓冲,随后 B 会和颜色缓冲中的 A 进行混合,这样混合结果会完全反过来,看起来就好像 B 在 A 的前面, 得到的就是错误的半透明结果。

因此半透明物体之间也是要符合一定的渲染顺序的。基于这两点,渲染引擎一般都会先对物体进行排序,再渲染。常用的方法是:

  1. 先渲染所有不透明物体,并开启它们的深度测试和深度写入
  2. 把半透明物体按它们距离摄像机的远近进行排序,然后按照从后往前的顺序渲染这些半透明物体,并开启它们的深度测试,但关闭深度写入

现在的问题是如何排序?我们在学习 Z-Buffer 时遇到过循环遮挡的现象,如下图:

image-20220403172326037

深度是对于每一个像素而言的,但对于一个物体我们怎么确定它的深度值呢?这种时候,我们可以选择把物体拆分成两个部分,然后再进行正确的排序。但是将物体分成两个部分还会存在物体的网格之间互相遮挡的现象,比如:

image-20220403172554846

上图中代表了两个网格,红色点分别标明了网格上距离摄像机最近的点、最远的点以及网格中点,由于一个物体的网格结构往往占据了空间中的某一块区域,也就是说,这个网格上每一个点的深度值可能都是不一样的, 我们选择哪个深度值来作为整个物体的深度值和其他物体进行排序呢?对于上图中的情况,选择哪个深度值都会得到错误的结果,我们的排序结果总是 A 在 B 的前面,但实际上 A 有一部分被 B 遮挡了。这也意味着,一旦选定了一种判断方式后,在某些情况下半透明物体之间一定会出现错误的遮挡问题。这种问题的解决方法通常也是再对网格进行分割。

尽管结论是,总是会有一些情况打乱我们的阵脚,但由于上述方法足够有效并且容易实现,因此大多数游戏引擎都使用了这样的方法。为了减少错误排序的情况,我们可以尽可能让模型是凸面体,并且考虑将复杂的模型拆分成可以独立排序的多个子模型等。其实就算排序错误结果有时也不会非常糟糕,如果我们不想分割网格,可以试着让透明通道更加柔和,使穿插看起来并不是那么明显。

在 Unity 中,提供了渲染队列(render queue)来解决渲染顺序问题。我们可以使用 SubShader 的 Queue 标签来决定我们的模型将归于哪个渲染队列。Unity 在内部使用一系列整数索引来表示每个渲染队列,且索引号越小表示越早被渲染。下表给出了这 5 个提前定义的渲染队列以及它们的描述:

image-20220403173042420

2 实现透明度测试

我们使用的纹理是一张透明纹理,每个方格的透明度都不同:

image-20220403175423766

通常,我们会在片元着色器中使用 clip 函数来进行透明度测试。clip 是 CG 中的一个函数,它的定义如下:

函数:void clip (float4 x); void clip (float3 x); void clip (float2 x); void clip (float1 x); void clip (float x);
参数:裁剪时使用的标量或矢量条件。
描述:如果给定参数的任何一个分量是负数,就会舍弃当前像素的输出颜色。它等同于下面的代码:

1
2
3
4
void clip (float4 x)
{
if (any(x < 0)) discard;
}

透明度测试的代码如下:

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
Shader "Unity Shaders Book/Chapter 8/Alpha Test" {
Properties {
_Color ("Color Tint", Color) = (1, 1, 1, 1)
_MainTex ("Main Tex", 2D) = "white" {}
// 定义一个控制透明度测试阈值的属性,范围在 [0,1],因为纹理像素的透明度就是在此范围内
_Cutoff ("Alpha Cutoff", Range(0, 1)) = 0.5
}
SubShader {
// 使用透明度测试的Shader要使用AlphaTest渲染队列
// IgnoreProjector设置为True, 这意味着这个Shader不会受到投影器(Projectors)的影响
// RenderType标签可以让Unity 把这个Shader 归入到提前定义的组(这里就是TransparentCutout组)中,以指明该Shader是一个使用了透明度测试的Shader
// 通常,使用了透明度测试的Shader都应该在SubShader中设置这三个标签
Tags {"Queue"="AlphaTest" "IgnoreProjector"="True" "RenderType"="TransparentCutout"}

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

CGPROGRAM

#pragma vertex vert
#pragma fragment frag

#include "Lighting.cginc"

fixed4 _Color;
sampler2D _MainTex;
float4 _MainTex_ST;
fixed _Cutoff;

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

struct v2f {
float4 pos : SV_POSITION;
float3 worldNormal : TEXCOORD0;
float3 worldPos : TEXCOORD1;
float2 uv : TEXCOORD2;
};

v2f vert(a2v v) {
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);

o.worldNormal = UnityObjectToWorldNormal(v.normal);

o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;

o.uv = TRANSFORM_TEX(v.texcoord, _MainTex);

return o;
}

fixed4 frag(v2f i) : SV_Target {
fixed3 worldNormal = normalize(i.worldNormal);
fixed3 worldLightDir = normalize(UnityWorldSpaceLightDir(i.worldPos));

fixed4 texColor = tex2D(_MainTex, i.uv);

// 进行透明度测试
clip (texColor.a - _Cutoff);
// 上面的一行相当于
// if ((texColor.a - _Cutoff) < 0.0) {
// discard;
// }

fixed3 albedo = texColor.rgb * _Color.rgb;

fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz * albedo;

fixed3 diffuse = _LightColor0.rgb * albedo * max(0, dot(worldNormal, worldLightDir));

return fixed4(ambient + diffuse, 1.0);
}

ENDCG
}
}
FallBack "Transparent/Cutout/VertexLit"
}

阈值 Alpha Cutoff = 0.55 时的效果:

image-20220403175744418

阈值 Alpha Cutoff = 0.7 时的效果:

image-20220403175811916

阈值 Alpha Cutoff = 0.9 时的效果:

image-20220403175824478

可以看出,透明度测试得到的透明效果很“极端”一一要么完全透明,要么完全不透明,它的效果往往像在一个不透明物体上挖了一个空洞。而且,得到的透明效果在边缘处往往参差不齐,有锯齿,这是因为在边界处纹理的透明度的变化精度问题。为了得到更加柔滑的透明效果,就应该使用透明度混合。

3 实现透明度混合

透明度混合的实现要比透明度测试复杂一些,这是因为我们在处理透明度测试时,实际上跟对待普通的不透明物体几乎是一样的,只是在片元着色器中增加了对透明度判断并裁剪片元的代码。而想要实现透明度混合就没有这么简单了。我们回顾之前提到的透明度混合的原理:它会使用当前片元的透明度作为混合因子,与已经存储在颜色缓冲中的颜色值进行混合,得到新的颜色。

为了进行混合,我们需要使用 Unity 提供的混合命令 Blend,Blend 是 Unity 提供的设置混合模式的命令。想要实现半透明的效果就需要把当前自身的颜色和已经存在于颜色缓冲中的颜色值进行混合,混合时使用的函数就是由该指令决定的。下表给出了 Blend 命令的语义:

image-20220403180134386

透明度混合的代码如下:

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
Shader "Unity Shaders Book/Chapter 8/Alpha Blend" {
Properties {
_Color ("Color Tint", Color) = (1, 1, 1, 1)
_MainTex ("Main Tex", 2D) = "white" {}
//_AlphaScale 用于在透明纹理的基础上控制整体透明度
_AlphaScale ("Alpha Scale", Range(0, 1)) = 1
}
SubShader {
// 使用透明度混合的Shader要使用Transparent渲染队列
// 渲染类型也是Transparent,这时会将Shader归入Transparent组中
// 同样开启IgnoreProjector
Tags {"Queue"="Transparent" "IgnoreProjector"="True" "RenderType"="Transparent"}

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

// 关闭深度写入
ZWrite Off
// 设置混合状态,SrcAlpha表示原纹理中的透明度,OneMinusSrcAlpha表示 1 - SrcAlpha
// 将原纹理中的透明度SrcAlpha作为源颜色因子,1 - SrcAlpha作为目标颜色因子
Blend SrcAlpha OneMinusSrcAlpha

CGPROGRAM

#pragma vertex vert
#pragma fragment frag

#include "Lighting.cginc"

fixed4 _Color;
sampler2D _MainTex;
float4 _MainTex_ST;
fixed _AlphaScale;

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

struct v2f {
float4 pos : SV_POSITION;
float3 worldNormal : TEXCOORD0;
float3 worldPos : TEXCOORD1;
float2 uv : TEXCOORD2;
};

v2f vert(a2v v) {
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);

o.worldNormal = UnityObjectToWorldNormal(v.normal);

o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;

o.uv = TRANSFORM_TEX(v.texcoord, _MainTex);

return o;
}

fixed4 frag(v2f i) : SV_Target {
fixed3 worldNormal = normalize(i.worldNormal);
fixed3 worldLightDir = normalize(UnityWorldSpaceLightDir(i.worldPos));

fixed4 texColor = tex2D(_MainTex, i.uv);

fixed3 albedo = texColor.rgb * _Color.rgb;

fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz * albedo;

fixed3 diffuse = _LightColor0.rgb * albedo * max(0, dot(worldNormal, worldLightDir));

// 修改了返回颜色中的透明通道的值,使用纹理的透明通道和透明系数的乘积
return fixed4(ambient + diffuse, texColor.a * _AlphaScale);
}

ENDCG
}
}
FallBack "Transparent/VertexLit"
}

Alpha Scale = 1 时的渲染效果:

image-20220403181200047

Alpha Scale = 0.5 时的渲染效果:

image-20220403181244334

Alpha Scale = 0.2 时的渲染效果:

image-20220403181300024

4 开启深度写入的半透明效果

当模型本身有复杂的遮挡关系或是包含了复杂的非凸网格的时候,就会有各种各样因为排序错误而产生的错误的透明效果,如下图:

image-20220403181435245

之前提到解决这样问题的方案是分割网格,但很多情况下这是不切实际的。这时,我们可以想办法重新利用深度写入,让模型可以像
半透明物体一样进行淡入淡出。

一种解决方案是,使用两个 Pass 来渲染模型:第一个 Pass 开启深度写入,但不输出颜色,它的目的仅仅是为了把该模型的深度值写入深度缓冲中;第二个 Pass 进行正常的透明度混合,由于上一个 Pass 已经得到了逐像素的正确的深度信息,该 Pass 就可以按照像素级别的深度排序结果进行透明渲染。但这种方法的缺点在于,多使用一个 Pass 会对性能造成一定的影响。

为了使一个 Pass 中不输出任何颜色,需要用到一个渲染指令 ColorMask,在 ShaderLab 中, ColorMask 用于设置颜色通道的写掩码(write mask),它的语义如下:

ColorMask RGB I A I 0 I 其他任何 R、G、B、A 的组合

当 ColorMask 设为 0 时,意味着该 Pass 不写入任何颜色通道,即不会输出任何颜色。这正是我们需要的,该 Pass 只需写入深度缓存即可。

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
Shader "Unity Shaders Book/Chapter 8/Alpha Blend" {
Properties {
_Color ("Color Tint", Color) = (1, 1, 1, 1)
_MainTex ("Main Tex", 2D) = "white" {}
//_AlphaScale 用于在透明纹理的基础上控制整体透明度
_AlphaScale ("Alpha Scale", Range(0, 1)) = 1
}
SubShader {
// 使用透明度混合的Shader要使用Transparent渲染队列
// 渲染类型也是Transparent,这时会将Shader归入Transparent组中
// 同样开启IgnoreProjector
Tags {"Queue"="Transparent" "IgnoreProjector"="True" "RenderType"="Transparent"}

// 第一个 Pass 不做任何操作,只把模型的深度信息写入深度缓存
Pass {
// 打开深度写入
ZWrite On
// ColorMask用于设置颜色通道的写掩码,0则不写入任何颜色通道
ColorMask 0
}
// 第二个 Pass 和正常的透明度混合一样
Pass {
Tags { "LightMode"="ForwardBase" }

// 关闭深度写入
ZWrite Off
// 设置混合状态,SrcAlpha表示原纹理中的透明度,OneMinusSrcAlpha表示 1 - SrcAlpha
// 将原纹理中的透明度SrcAlpha作为源颜色因子,1 - SrcAlpha作为目标颜色因子
Blend SrcAlpha OneMinusSrcAlpha

CGPROGRAM

#pragma vertex vert
#pragma fragment frag

#include "Lighting.cginc"

fixed4 _Color;
sampler2D _MainTex;
float4 _MainTex_ST;
fixed _AlphaScale;

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

struct v2f {
float4 pos : SV_POSITION;
float3 worldNormal : TEXCOORD0;
float3 worldPos : TEXCOORD1;
float2 uv : TEXCOORD2;
};

v2f vert(a2v v) {
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);

o.worldNormal = UnityObjectToWorldNormal(v.normal);

o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;

o.uv = TRANSFORM_TEX(v.texcoord, _MainTex);

return o;
}

fixed4 frag(v2f i) : SV_Target {
fixed3 worldNormal = normalize(i.worldNormal);
fixed3 worldLightDir = normalize(UnityWorldSpaceLightDir(i.worldPos));

fixed4 texColor = tex2D(_MainTex, i.uv);

fixed3 albedo = texColor.rgb * _Color.rgb;

fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz * albedo;

fixed3 diffuse = _LightColor0.rgb * albedo * max(0, dot(worldNormal, worldLightDir));

// 修改了返回颜色中的透明通道的值,使用纹理的透明通道和透明系数的乘积
return fixed4(ambient + diffuse, texColor.a * _AlphaScale);
}

ENDCG
}
}
FallBack "Transparent/VertexLit"
}

开启深度写入的透明度混合效果如下:

image-20220403194318704

相比于之前的结果,我们得到了正确的透明关系。

5 混合命令

之前我们已经用过了 ShaderLab 提供的混合命令。实际上,混合还有很多其他用处,不仅仅是用于透明度混合。因此我们有必要更加详细地了解混合中的细节问题。

我们首先来看一下混合是如何实现的。当片元着色器产生一个颜色的时候,可以选择与颜色缓存中的颜色进行混合。这样一来,混合就和两个操作数有关:源颜色(source color)和目标颜色(destination color)。源颜色我们用 S 表示,指的是由片元着色器产生的颜色值;目标颜色我们用 D 表示,指的是从颜色缓冲中读取到的颜色值。对它们进行混合后得到的输出颜色,我们用 O 表示,它会重新写入到颜色缓冲中。需要注意的是,当我们谈及混合中的源颜色、目标颜色和输出颜色时,它们都包含了 RGBA 四个通道的值,而并非仅仅是 RGB 通道。

想要使用混合,我们必须首先开启它。在 Unity 中,当我们使用 Blend (Blend Off 命令除外)命令时,除了设置混合状态外也开启了混合。但是,在其他图形 API 中我们是需要手动开启的。例如在 OpenGL 中,我们需要使用 glEnable(GL_BLEND) 来开启混合。但在 Unity 中,它已经在背后为我们做了这些工作。

我们之前设置混合因子只使用了两个因子 SrcFactor 和 DstFactor,这意味着对于 RGBA 四个通道使用相同的因子混合:
$$
O_{RGBA} = SrcFactor * S_{RGBA} +DstFactor * D_{RGBA}
$$
我们当然也可以单独设置 A 通道的混合因子。所有这些混合因子可取的值包括:

参数 描述
One 因子为 1
Zero 因子为 0
SrcColor 因子为源颜色值。当用于混合 RGB 的混合等式时,使用 SrcColor 的 RGB 分量作为混合因子:当用于混合 A 的混合等式时,使用 SrcColor 的 A 分量作为混合因子。
SrcAlpha 因子为源颜色的透明度值(A 通道)
DstColor 因子为目标颜色值。当用于混合 RGB 的混合等式时,使用 DstColor 的 RGB 分量作为混合因子:当用于混合 A 的混合等式时,使用 DstColor 的 A 分量作为混合因子。
DstAlpha 因子为目标颜色的透明度值(A 通道)
OneMinusSrcColor 因子为 (1 - 源颜色)。当用于混合 RGB 的混合等式时,使用结果的 RGB 分量作为混合因子:当用于混合 A 的混合等式时,使用结果的 A 分量作为混合因子。
OneMinusSrcAlpha 因子为 (1 - 源颜色的透明度值)
OneMinusDstColor 因子为 (1 - 目标颜色)。当用于混合 RGB 的混合等式时,使用结果的 RGB 分量作为混合因子:当用于混合 A 的混合等式时,使用结果的 A 分量作为混合因子。
OneMinusDstAlpha 因子为 (1 - 目标颜色的透明度值)

上面的混合等式默认使用的是加操作,实际上还支持其他的混合操作,我们可以使用 ShaderLab 的 BlendOp BlendOperation 命令, 即混合操作命令来设置。支持的操作有:

操作 描述
Add 将混合后的源颜色和混合后的目标颜色相加。默认的混合操作。
Sub 用混合后的源颜色减去混合后的目标颜色。
RevSub 用混合后的目标颜色减去混合后的源颜色。
Min 使用源颜色和目标颜色中较小的值,是逐分量比较的。
Max 使用源颜色和目标颜色中较大的值,是逐分量比较的。
其他逻辑操作 仅在 DirectX 11.1 以上支持

使用 Min 和 Max 混合操作时混合因子没有作用。

常用的不同混合操作设置如下:

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
// 正常的透明度混合
Blend SrcAlpha OneMinusSrcAlpha

// 柔和相加,Soft Additive
Blend OneMinusDstColor One

// 正片叠底,即相乘,Multiply
Blend DstColor Zero

// 两倍相乘,2x Multiply
Blend DstColor SrcColor

// 变暗,Darken
BlendOp Min

// 变亮,Lighten
BlendOp Max

// 滤色,Screen
Blend OneMinusDstColor One
// 另一种滤色
Blend One OneMinusSrcColor

// 线性减淡,Linear Dodge
Blend One One

各种混合效果如下:

image-20220403201017357

6 双面渲染的透明效果

在现实生活中,如果一个物体是透明的,意味着我们不仅可以透过它看到其他物体的样子,也可以看到它内部的结构。但在前面实现的透明效果中,无论是透明度测试还是透明度混合,我们都无法观察到正方体内部及其背面的形状,导致物体看起来就好像只有半个一样。这是因为,默认情况下渲染引擎剔除了物体背面(相对于摄像机的方向)的渲染图元,而只渲染了物体的正面。如果我们想要得到双面渲染的效果,可以使用 Cull 指令来控制需要剔除哪个面的渲染图元。在 Unity 中, Cull 指令的语法如下:

Cull Back I Front I Off

如果设置为 Back,那么那些背对着摄像机的渲染图元就不会被渲染,这也是默认情况下的剔除状态;如果设置为 Front,那么那些朝向摄像机的渲染图元就不会被渲染;如果设置为 Off,就会关闭剔除功能,那么所有的渲染图元都会被渲染,但由于这时需要渲染的图元数目会成倍增加,因此除非是用于特殊效果,例如这里的双面渲染的透明效果,通常情况是不会关闭剔除功能的。

我们在之前的透明度测试代码的 Pass 中加上一句:

1
2
// 关闭图元剔除
Cull Off

得到的效果如下:

image-20220403201750670

此时可以透过透明部分看到正方体内部的图元了。

和透明度测试相比,想要让透明度混合实现双面渲染会更复杂一些,这是因为透明度混合需要关闭深度写入,因此渲染顺序很重要。如果直接关闭剔除功能,那么我们就无法保证同一个物体的正面和背面图元的渲染顺序,就有可能得到错误的半透明效果。为此,我们选择把双面渲染的工作分成两个 Pass ——第一个 Pass 只渲染背面,第二个 Pass 只渲染正面,由于 Unity 会顺序执行 SubShader 中的各个 Pass,因此我们可以保证背面总是在正面被渲染之前渲染,从而可以保证正确的深度渲染关系。

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
Shader "Unity Shaders Book/Chapter 8/Alpha Blend With Both Side" {
Properties {
_Color ("Color Tint", Color) = (1, 1, 1, 1)
_MainTex ("Main Tex", 2D) = "white" {}
_AlphaScale ("Alpha Scale", 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

// 和之前一样的代码

ENDCG
}

// 第二个 Pass 渲染正面
Pass {
Tags { "LightMode"="ForwardBase" }

ZWrite Off
Blend SrcAlpha OneMinusSrcAlpha
// 剔除背面图元
Cull Back

CGPROGRAM

// 和之前一样的代码

ENDCG
}
}
FallBack "Transparent/VertexLit"
}

最终得到的透明度混合的效果如下:

image-20220403202712663

---- 本文结束 知识又增加了亿点点!----

文章版权声明 1、博客名称:LycTechStack
2、博客网址:https://lz328.github.io/LycTechStack.github.io/
3、本博客的文章部分内容可能来源于网络,仅供大家学习与参考,如有侵权,请联系博主进行删除处理。
4、本博客所有文章版权归博主所有,如需转载请标明出处。