《Unity Shader入门精要》八、透明效果

240 阅读12分钟
  • 在 Unity 中,通常用两种方法来实现透明效果
    • 透明度测试(Alpha Text),但其实它无法真正实现半透明
    • 透明度混合(Alpha Blending)
  • 对于不透明物体,不考虑它们的渲染顺序也能得到正确的排序效果(先 A 后 B 或者先 B 后 A 都可以),这是因为强大的深度缓冲的存在
    • 思路:将待渲染片元的深度值与深度缓冲中的值进行比较,来决定是否要显示。若显示,除了覆盖掉此时颜色缓冲中的像素值外,还要将它的深度值更新到深度缓冲中
  • 若想要实现透明效果,事情就没这么简单了,因为当使用了透明度混合时,我们关闭了深度写入(ZWrite)

两种实现方法的原理:

  • 透明度测试:只要一个片元的透明度不满足条件,就会被舍弃。这方法不需要关闭深度写入,片元通过后会按普通不透明物体的处理方式来处理,但产生的效果比较极端,要么完全透明,要么完全不透明
  • 透明度混合:这方法可得到真正的半透明效果。它会使用当前片元的透明度作为混合因子,与颜色缓冲中的颜色值进行混合,得到新的颜色。但这方法需要关闭深度写入,使用我们要非常小心物体的渲染顺序

8.1 为什么渲染顺序很重要(对于透明度混合)

  • 若在进行透明度混合时不关闭深度写入,一个半透明物体后面的物体将会被剔除,得到不正确的渲染效果

  • 我们不得不关闭深度写入破坏了深度缓冲的机制,这是非常非常非常糟糕的事情

  • 来考虑不同的渲染顺序会有什么结果(透明物体与不透明物体)

    image.png

    1. 先 B 后 A。B 首先写入颜色缓冲深度缓冲,然后半透明 A 仍然会进行深度测试,发现它比 B 离摄像机更近,需要被渲染,就将 A 与颜色缓冲中的 B 颜色进行混合,得到正确的效果

    2. 先 A 后 B。半透明 A 首先写入颜色缓冲,但由于关闭了深度写入而没有修改深度缓冲。然后到渲染 B 进行深度测试时,它发现深度缓冲非常干净,就通过测试并写入颜色缓冲和深度缓冲了,从而直接覆盖掉原来 A 的颜色,得到错误的效果

    • 由此可知:我们应该在不透明物体渲染之后再渲染半透明物体
  • 来考虑不同的渲染顺序会有什么结果(全是透明物体)

    1. 先 B 后 A。B 先写入颜色缓冲,然后 A 会和颜色缓冲中的 B 颜色进行混合,得到正确的效果
    2. 先 A 后 B。A 先写入颜色缓冲,然后 B 会和颜色缓冲中的 A 颜色进行混合,但这样混合看起来是 B 在 A 的前面,得到错误的效果
    • 由此可知:半透明物体之间也要按一定的顺序来渲染

渲染引擎一般会先对物体进行排序,再渲染。常用方法是

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

但这样还是没解决全部问题。若一半透明物体不是完全在另一半透明物体的前面/后面时,就会出现循环重叠的情况。这时我们可以选择把物体拆分成两部分然后再进行正确的排序

image.png

但是还会有其他的情况来“捣乱”(平面俯/侧着放,导致平面上有近摄像机点和远摄像机点)

image.png

  • 网格上每个点的深度值可能都不一样,我们选择哪个深度值来排序呢?
  • 选择哪个都会得到错误的结果,排序的结果总是 A 在 B 前面,但实际上 A 有一部分被 B 挡住
  • 这种问题的解决方法通常也是分割网格

大多数游戏引擎都使用这样的方法

  • 尽可能让模型是凸面体,以减少错误排序的情况
  • 考虑将复杂模型拆分成可以独立排序的多个子模型
  • 不想分割网格
    • 试着让透明通道更柔和,使穿插看起来挺“正常”的
    • 开启深度写入,近似地模拟半透明效果

8.2 Unity Shader 的渲染顺序

Unity 为了解决渲染顺序问题提供了渲染队列(render queue)解决方案

  • 可使用 SubShader 的 Queue 标签来决定模型将归于哪个渲染队列
  • 内部使用一系列整数索引来表示每个渲染队列,值越小优先级越高(越早渲染)

image.png

通过透明度测试来实现透明效果

image.png

通过透明度混合来实现透明效果

image.png

8.3 透明度测试

只要片元的透明度不满足条件(通常是小于某个值),那么它就会被舍弃。被舍弃的片元将不会再进行包括修改颜色缓冲在内的任何处理,否则就会按普通的不透明物体的方式来处理

通常会在片元着色器中使用 clip 函数来进行透明度测试

步骤:

  1. 老 4 步(建场景,去天空,建材质,建 Shader 并赋给材质)

  2. 场景中建立方体,将材质赋给它。在立方体下创建一个平面

  3. Shader 代码:源码

    • Properties 语义块

      image.png

      • _Cutoff:用于决定透明度测试时使用的判断条件(小于该值就舍弃片元)
    • SubShader 语义块中定义 Pass 语义块

      image.png

      • Queue 标签指定渲染队列名为 AlphaTest
      • RenderType 标签可让 Unity 把这个 Shader 归入到提前定义的组(TransparentCutout),以指明该 Shader 是一个使用了透明度测试的 Shader
      • IgnoreProjector 设为 True,意味着这个 Shader 不受投影器的影响
      • 通常,使用了透明度测试的 Shader 都应该设置这三个标签
    • 顶点着色器的输入和输出结构体(打算在世界空间中进行计算光照)

      image.png

    • 顶点着色器

      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"
      
      • 不仅能保证显卡兼容性,还可保证使用透明度测试的物体可以正确地向其他物体投向阴影

效果图:

image.png

问题:

  • 效果“极端”,要么不透明,要么完全透明
  • 边缘处往往参差不齐有锯齿

8.4 透明度混合

使用当前片元透明度作为混合因子,与颜色缓冲中的颜色进行混合。但是需要关闭深度写入,因此需非常小心物体的渲染顺序

我们需要使用 Unity 提供的混合命令——Blend

image.png

我们会使用第二种语义。Unity 在我们使用 Blend 命令时会自动打开混合模式

混合公式为:

DstColornew=SrcFactor×SrcColor+DstFactor×DstColoroldDstColor_{new}=SrcFactor\times SrcColor+DstFactor\times DstColor_{old}

在 Shader 中这样使用

Blend SrcAlpha OneMinusSrcAlpha
  • SrcAlpha:源颜色的透明度(0-1),将它作为 SrcFactor,设为命令的第一个参数
  • OneMinusSrcAlpha:1 减源颜色的透明度,将它作为 DstFactor,设为命令的第二个参数

混合得到最终颜色的公式:

DstColornew=SrcAlpha×SrcColor+(1SrcAlpha)×DstColoroldDstColor_{new}=SrcAlpha\times SrcColor+(1-SrcAlpha)\times DstColor_{old}

实现步骤:

  1. 老 4 步
  2. 场景中建立方体,将材质赋给它。创建一个平面,移到立方体下面
  3. Shader 代码,将 8.3 节的 Shader 代码全部粘贴过去,只需要稍作修改即可:源码
    • Properties 语义块

      image.png

      • _AlphaScale:替代原来的 _Cutoff,在透明纹理的基础上控制整体透明度
    • Pass 中修改和属性对应的变量

      image.png

    • 修改 SubShader 标签

      image.png

      • Queue 标签指定渲染队列名为 Transparent
      • RenderType 标签可让 Unity 把这个 Shader 归入到提前定义的组(Transparent),以指明该 Shader 是一个使用了透明度混合的 Shader
      • IgnoreProjector 设为 True,意味着这个 Shader 不受投影器的影响
      • 通常使用了透明度混合的 Shader 都应设置上面 3 个标签
    • 在 Pass 中为透明度混合设置合适的混合状态

      image.png

      • ZWrite Off:关闭深度写入(透明度混合必须关闭)
      • 混合因子:
        • 源颜色的混合因子(第一个参数)设为 SrcAlpha
        • 目标颜色的混合因子(第二个参数)设为 OneMinusSrcAlpha
    • 顶点着色器不变

    • 修改片元着色器

      image.png

      • 移除了透明度测试代码
      • 将返回值中的透明通道,设为纹素的透明通道和材质参数 _AlphaScale 的乘积
    • 修改 Fallback

      image.png

效果

image.png

关闭深度写入,我们就无法对模型进行像素级别的深度排序,从而带来各种问题,像下图

image.png

分割网格听起来可行,但往往不切实际。这时我们可以想办法重新利用深度写入,来解决这些问题

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

8.4 最后的渲染问题,一种解决方法是使用两个 PASS来渲染:

  • 第一个 PASS 开启深度写入,但不输出颜色,目的仅仅为了将模型的深度值写入深度缓存中
  • 第二个 PASS 进行正常的透明度混合,由于上一个 PASS 已经得到逐像素正确的深度信息,这个 PASS 就可以按照像素级别的深度排序结果来进行透明渲染

缺点:多使用一个 PASS 会对性能造成影响

步骤:

  1. 老 4 步

  2. 建立方体,将材质赋给它。建平面,移动立方体下面

  3. Shader 代码,将 8.4 的 Shader 代码全粘贴过去,稍微修改一下:源码

    • 在原 PASS 上增加一个 PASS

      image.png

      • 新增的 PASS 的目的上面已经讲过,用来剔除模型中被自身遮挡的片元

      • 该 PASS 中的第二行使用了一个新的渲染命令——ColorMask,它用于设置颜色通道的写掩码(write mask)。语义如下:

        ColorMask RGB | A | 0 | 其他任何 RGBA 的组合

        • 当 ColorMask 设为 0 时,意味着该 PASS 不写入任何颜色通道,即不会输出任何颜色

效果对比

image.png

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 个因子

image.png

发现第一个命令只提供了两个因子,意味着将使用同样的混合因子代入两个混合等式

image.png

ShaderLab 支持的混合因子

image.png

有时我们希望使用不同的参数混合 A 通道,这是就可使用 Blend SrcFactor DstFactor SrcFactorA DstFactorA 指令来实现。

例如想要输出颜色的透明度等于源颜色的透明度,即

Oa=SrcFactorA×Sa+DstFactorA×Da=SaO_a=SrcFactorA \times S_a + DstFactorA \times D_a=S_a

那么只要令 SrcFactorA 等于 1,令 DstFactorA 等于 0 即可。对应的命令为:

Blend SrcAlpha OneMinusSrcAlpha One Zero

8.6.2 混合操作

我们可使用 ShaderLab 的混合操作命令—— BlendOp BlendOperation 命令,来改变混合等式的操作符(默认为加)

image.png

混合操作命令通常是与混合因子一起工作的(Min 和 Max 除外)

8.6.3 常见的混合类型

image.png

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 小节的代码,做两处修改 源码

  1. 复制原 Pass 代码,得到另一个 Pass

  2. 在两个 Pass 中分别使用 Cull 指令剔除不同朝向的渲染图元(先剔除 Front,再剔除 Back)

效果