百人计划-透明效果

参考资料:
《Unity Shader入门精要》 第8章 透明效果

代码标注为C++只是为了会有高亮好看,实际是ShaderLab

概述

我们通常使用两种方法来实现透明效果:

  1. 使用透明度测试(Alpha Test), 这种方法其实无法得到真正的半透明效果;
  2. 透明度混合 (Alpha Blending)

透明度测试(Alpha Test)

透明度测试简单粗暴,只要个片元的透明度不满足条件(通常是小千某个阙值),那么它对应的片元就会被舍弃。被舍弃的片元将不会再进行任何处理,也不会对颜色缓冲产生任何影响;否则,就会按照普通的不透明物体的处理方式来处理它,即进行深度测试、深度写入等。

透明度测试通过函数 void clip(float4 x) 实现剔除。

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

具体的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
Shader "..."{
Properties{
_Color("Color", color)=(1,1,1,1)
_MainTex("Tex", 2D)="white"{}
_AlphaCutoff("Alpha Cutoff", Range(0, 1))=0.5 // 因为Alpha通道是在0到1的范围
}
SubShader{
Tags{"Queue"="AlphaTest" "IgnoreProjector"="True" "RenderType"="TransparentCutout"}

Pass{
//...
fixed4 frag(in v2f i) : SV_TARGET{
fixed4 texColor = tex2D(_MainTex, i.uv);
// Alpha Test
clip(texColor.a - _AlphaCutoff);

// albedo
// ...
// ambient
// ...
// diffuse
// ...
return fixed4((ambient+diffuse), 1.0);
}
//...
}
}
}

简单归纳:

  1. 注意设置渲染队列
  2. 注意 texColor 要使用 fixed4 类型定义接收alpha通道的值
  3. 使用 clip 进行透明度测试

双面渲染的透明效果

真正的透明物体还可以看到物体内部的细节,显然上面的透明效果是错误的,因此就需要进行双面渲染。产生上面的效果的原因是Unity会 自动进行背面剔除 来提高渲染效率,不渲染背面,我们只需要关闭背面剔除即可:

1
2
3
4
Pass{
// 关闭背面剔除
Cull off
}

透明度混合(Alpha Blend)

透明度测试的方法很简单,只需要使用clip函数进行剔除即可,但是这种方法过于极端,只能得到完全不透明和完全透明两种效果,这在现实中是不太可能出现的。因此就引入了透明度混合的方法。

透明度混合的原理是:使用当前片元的透明度作为混合因子,与已经存储在颜色缓冲中的颜色值进行混合,得到最终的颜色。

渲染顺序

在进行透明效果的渲染时,渲染顺序就尤为重要,在渲染不透明物体时,深度写入(ZWrite)是默认打开的,这样就实现了正确的物体间遮挡关系,但是在渲染透明效果时,必须关闭深度写入,否则就会因为透明物体较其后方的物体深度较近而不渲染透明物体后的物体。

透明效果的原理是,对于透明效果的shader 关闭深度写入(ZWrite Off),这样透明物体的深度就一直是默认的最大值,无法遮挡任何物体。但是关闭深度写入之后就需要非常注意渲染的顺序。

如图8.1,如果先渲染透明物体A,A不会写入深度缓冲,当之后渲染非透明物体B时,B开启了深度写入发现深度缓冲还是默认值,就会同时将自己的深度写入深度缓冲,将自己的颜色写入颜色缓冲,就完全覆盖了透明物体A。
如图8.2,对于两个透明物体的情况,如果先渲染物体A,A发现深度缓冲是默认值,将自己的颜色写入颜色缓冲,但是不会写入深度缓冲;渲染物体B时,B也发现深度缓冲时默认值,会将自己的颜色和颜色缓冲中的颜色进行混合(使用透明度混合进行渲染时),看起来就是B在A前面

因此只要渲染的物体中有透明效果的物体,就需要注意渲染顺序。Unity中使用 渲染队列(Render Queue) 定义渲染顺序:

可以发现对于使用了透明度混合的方法渲染的透明物体需要按照 从后往前 的顺序依次渲染,原因我们已经分析过了,但是这个从后往前其实并不好判断,如图:

一般我们需要让模型尽量是凸面体,并且可以考虑将复杂的模型拆分成可以独立排序的多个子模型,但是一般游戏引擎还是使用最简单的判断方式,忽略了这些问题。

混合命令

实现混合需要使用unity内置的 Blend 命令:

如果使用了除Blend Off之外的混合命令,Unity会自动将混合模式开启,但是其他一些图形API中还需要手动开启。

Blend命令后的参数是混合因子,用来设定混合公式,Unity中有如下的混合因子:

BlendOp BlendOperation命令允许使用加法外的方法进行混合,支持如下操作(还有Max和Min类似):

基本实现

使用透明度混合实现透明效果的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
Shader"..."
{
Properties{
_Color("Color", color)=(1,1,1,1)
_MainTex("Main Tex", 2D)="white"{}
_AlphaScale("AlphaScale", Range(0, 1))=0.5
}
SubShader{
Tags{"Queue"="Transparent" "IgnoreProjector"="True" "RanderType"="Transparent"}

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

// 关闭深度写入,设置混合因子
ZWrite Off
Blend SrcAlpha OneMinusSrcAlpha

// ...
fixed4 frag(in v2f i) : SV_TARGET{
fixed4 texColor = tex2D(_MainTex, i.uv);
// albedo
fixed3 albedo = texColor.rgb * _Color.rgb;
// ambient
// ...
// diffuse
// ...
return fixed4((ambient+diffuse), texColor.a * _AlphaScale);
}
// ...
}
}
}

简单归纳:

  1. 设置渲染队列
  2. 关闭深度写入,设置混合因子
  3. 注意 texColor 使用fixed4接收Alpha通道
  4. 返回时的透明通道使用纹理的透明度(texColor.a)与透明度( _AlphaScale )相乘,这里的透明度只有开启混合才有效。

双面渲染的透明效果

与使用透明度测试类似,由于背面剔除,透明效果实际上是错误的,但是由于透明度混合关闭了深度写入,导致无法得到正确的深度信息,如果简单的关闭背面剔除,并不能得到正确的渲染结果。因此使用两个Pass分别渲染物体的背面和正面,注意要先渲染物体的背面再渲染正面!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Shader"..."{
Properties{
//...
}
SubShader{
Tags{"Queue"="Transparent" "IgnoreProjector"="True" "RenderType"="Transparent"}
Pass{
// 剔除正面,先渲染背面
Cull Front
//...
}
Pass{
// 剔除背面,后渲染正面
Cull Back
}
}
}

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

由于我们进行透明度混合时关闭了深度写入,导致对于一些复杂形状分不清物体内部的遮挡关系,会得到看起来奇怪的渲染结果:

对于这种情况,我们使用两个Pass渲染,第一个Pass仅开启深度写入将深度正确的记录到深度缓冲中,而不向颜色缓冲写入任何颜色;第二个Pass按照普通的透明度混合实现即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Shader"..."{
SubShader{
// ...
Pass{
ZWrite On
// 设置颜色通道的写掩码
ColorMask 0 // ColorMask RGB | A | 0 | 其他RGBA的任意组合
}
Pass{
// ...
ZWrite Off
// ...
}
}
}

“看上去”正确不少,但是还是不能透过物体看到内部。因为第一个Pass将深度写进深度缓冲后已经将背面和被遮挡的片元剔除了。


百人计划-透明效果
https://kenny-hoho.github.io/2023/08/05/百人计划-透明效果/
作者
Kenny-hoho
发布于
2023年8月5日
许可协议