- 在 Unity 中,通常用两种方法来实现透明效果
透明度测试(Alpha Text),但其实它无法真正实现半透明透明度混合(Alpha Blending)
- 对于不透明物体,不考虑它们的渲染顺序也能得到正确的排序效果(先 A 后 B 或者先 B 后 A 都可以),这是因为强大的
深度缓冲的存在- 思路:将待渲染片元的深度值与深度缓冲中的值进行比较,来决定是否要显示。若显示,除了覆盖掉此时颜色缓冲中的像素值外,还要将它的深度值更新到深度缓冲中
- 若想要实现透明效果,事情就没这么简单了,因为当使用了
透明度混合时,我们关闭了深度写入(ZWrite)
两种实现方法的原理:
- 透明度测试:只要一个片元的透明度不满足条件,就会被舍弃。这方法不需要关闭深度写入,片元通过后会按普通不透明物体的处理方式来处理,但产生的效果比较极端,要么完全透明,要么完全不透明
- 透明度混合:这方法可得到真正的半透明效果。它会使用当前片元的透明度作为混合因子,与颜色缓冲中的颜色值进行混合,得到新的颜色。但这方法需要关闭深度写入,使用我们要非常小心物体的渲染顺序
8.1 为什么渲染顺序很重要(对于透明度混合)
-
若在进行
透明度混合时不关闭深度写入,一个半透明物体后面的物体将会被剔除,得到不正确的渲染效果 -
我们不得不关闭
深度写入破坏了深度缓冲的机制,这是非常非常非常糟糕的事情 -
来考虑不同的渲染顺序会有什么结果(透明物体与不透明物体)
-
先 B 后 A。B 首先写入
颜色缓冲和深度缓冲,然后半透明 A 仍然会进行深度测试,发现它比 B 离摄像机更近,需要被渲染,就将 A 与颜色缓冲中的 B 颜色进行混合,得到正确的效果 -
先 A 后 B。半透明 A 首先写入颜色缓冲,但由于关闭了
深度写入而没有修改深度缓冲。然后到渲染 B 进行深度测试时,它发现深度缓冲非常干净,就通过测试并写入颜色缓冲和深度缓冲了,从而直接覆盖掉原来 A 的颜色,得到错误的效果
- 由此可知:我们应该在不透明物体渲染之后再渲染半透明物体
-
-
来考虑不同的渲染顺序会有什么结果(全是透明物体)
- 先 B 后 A。B 先写入
颜色缓冲,然后 A 会和颜色缓冲中的 B 颜色进行混合,得到正确的效果 - 先 A 后 B。A 先写入
颜色缓冲,然后 B 会和颜色缓冲中的 A 颜色进行混合,但这样混合看起来是 B 在 A 的前面,得到错误的效果
- 由此可知:半透明物体之间也要按一定的顺序来渲染
- 先 B 后 A。B 先写入
渲染引擎一般会先对物体进行排序,再渲染。常用方法是
- 先渲染所有不透明物体,并开启它们的深度测试和写入
- 把全部半透明物体按距离摄像机的远近进行排序,然后按照从后往前的顺序渲染它们,并开启它们的尝试测试,但关闭深度写入
但这样还是没解决全部问题。若一半透明物体不是完全在另一半透明物体的前面/后面时,就会出现循环重叠的情况。这时我们可以选择把物体拆分成两部分然后再进行正确的排序
但是还会有其他的情况来“捣乱”(平面俯/侧着放,导致平面上有近摄像机点和远摄像机点)
- 网格上每个点的深度值可能都不一样,我们选择哪个深度值来排序呢?
- 选择哪个都会得到错误的结果,排序的结果总是 A 在 B 前面,但实际上 A 有一部分被 B 挡住
- 这种问题的解决方法通常也是
分割网格
大多数游戏引擎都使用这样的方法
- 尽可能让模型是凸面体,以减少错误排序的情况
- 考虑将复杂模型拆分成可以独立排序的多个子模型
- 不想分割网格
- 试着让透明通道更柔和,使穿插看起来挺“正常”的
- 开启深度写入,近似地模拟半透明效果
8.2 Unity Shader 的渲染顺序
Unity 为了解决渲染顺序问题提供了渲染队列(render queue)解决方案
- 可使用 SubShader 的 Queue 标签来决定模型将归于哪个渲染队列
- 内部使用一系列整数索引来表示每个渲染队列,值越小优先级越高(越早渲染)
通过透明度测试来实现透明效果
通过透明度混合来实现透明效果
8.3 透明度测试
只要片元的透明度不满足条件(通常是小于某个值),那么它就会被舍弃。被舍弃的片元将不会再进行包括修改颜色缓冲在内的任何处理,否则就会按普通的不透明物体的方式来处理
通常会在片元着色器中使用 clip 函数来进行透明度测试
步骤:
-
老 4 步(建场景,去天空,建材质,建 Shader 并赋给材质)
-
场景中建立方体,将材质赋给它。在立方体下创建一个平面
-
Shader 代码:源码
-
Properties 语义块
- _Cutoff:用于决定透明度测试时使用的判断条件(小于该值就舍弃片元)
-
SubShader 语义块中定义 Pass 语义块
Queue标签指定渲染队列名为 AlphaTestRenderType标签可让 Unity 把这个 Shader 归入到提前定义的组(TransparentCutout),以指明该 Shader 是一个使用了透明度测试的 ShaderIgnoreProjector设为 True,意味着这个 Shader 不受投影器的影响- 通常,使用了
透明度测试的 Shader 都应该设置这三个标签
-
顶点着色器的输入和输出结构体(打算在世界空间中进行计算光照)
-
顶点着色器
v2f vert (a2v v) { v2f o; o.vertex = 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); fixed3 albedo = texColor.rgb * _Color.rgb; fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz * albedo; fixed3 diffuse = _LightColor0.rgb * albedo * (dot(worldNormal, worldLightDir) * 0.5 + 0.5); return fixed4(ambient + diffuse, 1.0); }- clip 函数,会判断参数是否为负数,是则舍弃该片元(效果就是完全透明)
-
设置合适的 Fallback
Fallback "Transparent/Cutout/VertexLit"- 不仅能保证显卡兼容性,还可保证使用透明度测试的物体可以正确地向其他物体投向阴影
-
效果图:
问题:
- 效果“极端”,要么不透明,要么完全透明
- 边缘处往往参差不齐有锯齿
8.4 透明度混合
使用当前片元透明度作为混合因子,与颜色缓冲中的颜色进行混合。但是需要关闭深度写入,因此需非常小心物体的渲染顺序
我们需要使用 Unity 提供的混合命令——Blend
我们会使用第二种语义。Unity 在我们使用 Blend 命令时会自动打开混合模式
混合公式为:
在 Shader 中这样使用
Blend SrcAlpha OneMinusSrcAlpha
- SrcAlpha:源颜色的透明度(0-1),将它作为 SrcFactor,设为命令的第一个参数
- OneMinusSrcAlpha:1 减源颜色的透明度,将它作为 DstFactor,设为命令的第二个参数
混合得到最终颜色的公式:
实现步骤:
- 老 4 步
- 场景中建立方体,将材质赋给它。创建一个平面,移到立方体下面
- Shader 代码,将 8.3 节的 Shader 代码全部粘贴过去,只需要稍作修改即可:源码
-
Properties 语义块
- _AlphaScale:替代原来的 _Cutoff,在透明纹理的基础上控制整体透明度
-
Pass 中修改和属性对应的变量
-
修改 SubShader 标签
Queue标签指定渲染队列名为 TransparentRenderType标签可让 Unity 把这个 Shader 归入到提前定义的组(Transparent),以指明该 Shader 是一个使用了透明度混合的 ShaderIgnoreProjector设为 True,意味着这个 Shader 不受投影器的影响- 通常使用了
透明度混合的 Shader 都应设置上面 3 个标签
-
在 Pass 中为透明度混合设置合适的混合状态
- ZWrite Off:关闭深度写入(透明度混合必须关闭)
- 混合因子:
- 将源颜色的混合因子(第一个参数)设为 SrcAlpha
- 将目标颜色的混合因子(第二个参数)设为 OneMinusSrcAlpha
-
顶点着色器不变
-
修改片元着色器
- 移除了透明度测试代码
- 将返回值中的透明通道,设为纹素的透明通道和材质参数 _AlphaScale 的乘积
-
修改 Fallback
-
效果
关闭深度写入,我们就无法对模型进行像素级别的深度排序,从而带来各种问题,像下图
分割网格听起来可行,但往往不切实际。这时我们可以想办法重新利用深度写入,来解决这些问题
8.5 开启深度写入的半透明效果
8.4 最后的渲染问题,一种解决方法是使用两个 PASS来渲染:
- 第一个 PASS 开启深度写入,但不输出颜色,目的仅仅为了将模型的深度值写入深度缓存中
- 第二个 PASS 进行正常的透明度混合,由于上一个 PASS 已经得到逐像素正确的深度信息,这个 PASS 就可以按照像素级别的深度排序结果来进行透明渲染
缺点:多使用一个 PASS 会对性能造成影响
步骤:
-
老 4 步
-
建立方体,将材质赋给它。建平面,移动立方体下面
-
Shader 代码,将 8.4 的 Shader 代码全粘贴过去,稍微修改一下:源码
-
在原 PASS 上增加一个 PASS
-
新增的 PASS 的目的上面已经讲过,用来剔除模型中被自身遮挡的片元
-
该 PASS 中的第二行使用了一个新的渲染命令——ColorMask,它用于设置颜色通道的写掩码(write mask)。语义如下:
ColorMask RGB | A | 0 | 其他任何 RGBA 的组合
- 当 ColorMask 设为 0 时,意味着该 PASS 不写入任何颜色通道,即不会输出任何颜色
-
-
效果对比
8.6 ShaderLab 的混合命令
混合还有很多其他用处而不仅仅是用于透明度混合
当片元着色器产生一个颜色时,可以选择与颜色缓存中的颜色进行混合。混合有关的两个参数:
源颜色(source color)。用 S 表示,指由片元着色器产生的颜色值目标颜色(destination color)。用 D 表示,指颜色缓冲中的颜色值
混合后得到的颜色,输出颜色(output color),用 O 表示,它会重新写入到颜色缓存中。这 3 个颜色值都包含 A 通道而不仅仅只有 RGB
想要使用混合,须先开启它。在 Unity 中使用 Blend 命令来设置混合状态(同时开启混合)。其他图形 API 是需要手动开启的
8.6.1 混合等式和参数
混合是一个逐片元操作,而它是不可编程的,但高度可配置。可改变这些配置来影响结果
- 使用的运算操作
- 混合因子
计算输出颜色的等式,称为混合等式。由于需要混合 RGB 通道和混合 A 通道,所以需要两个混合等式。
当设置混合状态时(如之前用到的 Blend SrcAlpha OneMinusSrcAlpha),实际上设置的就是混合等式中的操作和因子
- 默认情况下,操作都是加操作
- 每个等式有两个因子(一个与源颜色相乘,另一个与目标颜色相乘),故一共需要 4 个因子
发现第一个命令只提供了两个因子,意味着将使用同样的混合因子代入两个混合等式
ShaderLab 支持的混合因子
有时我们希望使用不同的参数混合 A 通道,这是就可使用 Blend SrcFactor DstFactor SrcFactorA DstFactorA 指令来实现。
例如想要输出颜色的透明度等于源颜色的透明度,即
那么只要令 SrcFactorA 等于 1,令 DstFactorA 等于 0 即可。对应的命令为:
Blend SrcAlpha OneMinusSrcAlpha One Zero
8.6.2 混合操作
我们可使用 ShaderLab 的混合操作命令—— BlendOp BlendOperation 命令,来改变混合等式的操作符(默认为加)
混合操作命令通常是与混合因子一起工作的(Min 和 Max 除外)
8.6.3 常见的混合类型
8.7 双面渲染的透明效果
前面的透明效果中,无论是哪种实现方法,我们都无法观察到物体内部及其背面的形状。这是因为默认情况下渲染引擎剔除了物体背面(相对于摄像机方向)的渲染图元。若想得到双面渲染的效果,可以使用 Cull 指令来控制需要剔除哪个面的图元
Cull Back | Front | Off
- Back:默认状态,背对摄像机的图元将被剔除不会被渲染
- Front:面向摄像机的图元将被剔除不会被渲染
- Off:关闭剔除功能,所有图元都会被渲染,所以待渲染图元数目会成倍增加。因此除非是特殊效果(如这里的双面渲染),通常情况下不会关闭此功能
8.7.1 透明度测试的双面渲染
实现非常简单,只要在 Pass 的渲染设置中使用 Cull 指令来关闭剔除即可
Shader 使用 8.3 小节的代码,在 Pass 中加一行

关闭剔除功能后,物体的所有图元都会被渲染。效果

8.7.2 透明度混合的双面渲染
分成两个 Pass
- 第一个只渲染背面
- 第二个只渲染正面
由于 Unity 会顺序执行 SubShader 中的各个 Pass,因此可以保证背面总是在正面之前渲染
实现方法,Shader 使用 8.4 小节的代码,做两处修改 源码
-
复制原 Pass 代码,得到另一个 Pass
-
在两个 Pass 中分别使用 Cull 指令剔除不同朝向的渲染图元(先剔除 Front,再剔除 Back)

效果
