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

渲染顺序

当场景中包含很多模型时,如ynci果所有模型都是不透明(opaque)的物体,那么由于强大的深度缓冲(depth buffer)的存在不考虑渲染顺序也可以得到正确的结果,但对于透明物体来说事情就没那么简单,因为使用透明度混合时,需要关闭深度写入(ZWrite)。如果不关闭深度写入,则会出现如下情况:比如一个半透明表面背后的表面本来是可以透过半透表面而被看到的,但是深度测试时,半透明表面的深度值距离摄像头更近,所以更新深度缓冲后,后面的表面会被剔除,我们也就看不到半透明表面的物体了,所以渲染顺序真的很重要。

重点说一下深度缓冲(depth buffer,又称z-buffer),它可以觉得哪个物体的哪些部分会被渲染在前面,哪些部分会被遮挡,所以它能够解决可见性(visibility)问题。它的基本思想是:当渲染一个片元时,就把该片元的深度值和已经存在在深度缓冲中的值进行比较(如果开启了深度测试),若它的值距离摄像机更远,则该片元不被渲染;否则,这个片元应该覆盖掉此时颜色缓冲中的颜色值,同时更新深度缓冲中的值。

所以一个比较通常的渲染顺序是:

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

但是,其实按照顺序排序本身就是一件不容易的事情。深度缓冲中的值是像素级别的,而对单个物体排序时会使得物体A与B的整个部分都是先后渲染的,那么如果物体间存在循环重叠的情况,则不可能得到正确的视觉效果

因此,为了减少错误的排序情况,实际中往往采用(1)网格分割,尽可能使物体是凸面体并且将复杂模型拆分成可以独立排序的多个子模型,(2)使通道更柔和,甚至(3)开启深度写入模拟半透明等方法来解决这些问题。

渲染队列

Unity中提供了渲染队列。我们可以使用SubShader的Queue标签来决定我们的模型归于哪个渲染队列。Unity在内部使用系列整数索引表示每个渲染队列,且索引号越小表示越早被渲染。

名称 队列索引号 描述
Background 1000 最早被渲染的物体的队列,通常用来绘制在背景上的物体。
Geometry 2000 不透明物体的渲染队列。大多数物体都应该使用该队列进行渲染,也是Unity Shader中默认的渲染队列
Alpha Test 2450 有透明通道,需要进行Alpha Test的物体的队列,比在Geomerty中更有效.
Transparent 3000 半透物体的渲染队列。一般是不写深度的物体,Alpha Blend等的在该队列渲染。
Overlay 4000 最后被渲染的物体的队列,一般是覆盖效果,比如镜头光晕,屏幕贴片之类的。

因此,如果想采用透明度测试实现透明效果,代码中应包含:

SubShader {
	Tags { "queue"="AlphaTest"}
	Pass {
		...
	}
}

透明度混合应包含:

SubShader {
	Tags { "queue"="Transparent"}
	Pass {
		ZWrite OFF
		...
	}
}

透明原理

前面提到实现透明效果的方法有两种,原理也不同。

  • 透明度测试:它采用一种“非舍即留”的机制,即,若某片元的透明度不满足条件(通常小于某个阈值),则舍弃;否则就按照不透明物体去处理。也就是说,透明度测试是不关闭深度写入的。它产生的结果要么完全透明,看不到;要么完全不透明,与其他不透明物体一样。
  • 透明度融合:这种方法可以得到真正的半透明效果。它会使用当前片元的透明度作为混合因子,与已经存储在颜色缓冲中的颜色值进行混合,得到新的颜色。它需要关闭深度写入,因而我们需要非常注意渲染的顺序。另外,透明度混合虽然关闭了深度写入,但没有关闭深度测试,只不过此时比较深度值将决定是否进行混合操作。在这种情况下,不透明物体出现在透明物体前的场景中,即使先渲染了不透明物体,也可以正常地遮挡住透明物体的部分。换句话说,对于透明度混合而言,深度是只读的。

后续两部分将就透明度测试,透明度混合,开启深度写入的透明度混合以及双面透明效果进行实践~

参考

Unity_Shaders_Book : https://github.com/candycat1992/Unity_Shaders_Book

Unity Manual: https://docs.unity3d.com/Manual/TextureTypes.html