本篇开始将进行《入门精要》初级篇最后一部分的学习,完成透明效果的学习。
1 Unity中实现透明效果的方法
一个像素的RGBA参数中的最后一项A指的就是透明度Alpha。在之前的实践中,一般值都默认是1,我们认为完全不透明,就像一个片元着色器输出的fixed4颜色值的第四个数值总会是1;当像素透明度为0时则表示像素完全不显示(完全透明)。Unity中实现透明效果通常有两种方法,透明度测试(Alpha Test)和透明度混合(Alpha Blending)。
1.1 透明度测试
1.1.1 原理
透明度测试原理很简单,就是在正常光照的基础上,加上一个float类型的_Cutoff透明度参考数值,定义一个完全透明/不透明的界限,一般可以定义“大于它的不透明,小于它的完全透明”。除此之外跟之前的光照步骤再没有任何的区别了,后续会给在Unity中给它实现出来。
1.1.2 特点
无需关闭深度写入——由于是直接根据每个片元RGBA的.a值判断是否留下(不透明)/裁剪掉(完全透明),相当于只是在片元着色器的步骤做改变,因此它无需关闭深度写入(ZWrite),关于什么是深度写入以及它与渲染顺序的关系,将在后面章节详细讲述。
透明效果挺“极端”——正因上述实现的方法,透明测试出的片元要么不透明,要么完全透明,得到的效果就像在物体上挖了一个洞!
透明效果存在锯齿——边界处往往会因为判断的精度问题存在锯齿,这在后面的实践过程中会有所体现。
1.2 透明度混合
1.2.1 原理
看到“混合”是不是很熟悉?没错!就在图形渲染管线3.0-光栅化和像素处理阶段中像素处理阶段的最后一个环节——混合(Blend)。
只有透明或者半透明的物体,才会需要混合操作。混合是混合源颜色和目标颜色,而透明度混合就是拿当前片元的透明度作为混合因子,与颜色缓冲区中的片元颜色值混合,得到新的颜色值。
1.2.2 特点
需要关闭深度写入,但不关闭深度测试——与透明度测试不同,透明度混合需要关闭深度写入,但深度测试继续进行。这意味着还会进行根据深度值判断“谁前谁后”的操作,
- 如果当前需要混合的片元“被挡住了”,那么就不需要进行混合了;
- 如果当前需要混合的片元“在前面”,就会进行混合操作,但不执行深度写入!
这个逻辑要搞明白!!至于为什么要这样做?在后面的渲染顺序中会进行详细叙述。
效果更加柔滑——透明度混合的边界效果将比透明度测试更加柔滑,我认为这是因为透明度混合执行的操作是颜色的混合而非直接“一刀切”的缘故,这样就不会涉及到判断的精度问题了。
无法对模型进行像素级别的排序——这种情况尤其是在面对模型网格之间有相互交叉的结构,会出现遮挡错误的半透明效果。
1.2.3 开启深度写入的透明度混合
对于关闭深度写入的透明度混合,无法对模型进行像素级别的深度排序,当模型网格之间有相互重叠的情况,往往会出现错误的效果,这一点将在第5节进行讨论,以及实现开启深度写入的透明度混合的方法。
1.3 双面渲染的效果*
一般地,引擎在进行渲染时,都默认剔除了物体背面的渲染图元,这个背面是相对于摄像机方向而言的,例如上述提到的透明度测试和透明度混合,都是仅实现了正面渲染。
为了实现双面,可以使用Unity中的Cull指令来控制需要剔除哪个面的渲染图元,这个将在后面的第6节展示。
2 渲染顺序
2.1 为什么关闭深度写入?
深度写入跟渲染管线3.0中提到的深度测试、模板测试一样,我们都可以自行开启/关闭,开启深度写入就代表着——默许用当前的片元深度值覆盖掉原有z-buffer的深度值。
那么上述的透明度混合操作,为什么需要关闭深度写入呢?——试想一下,一个正方体的前、后面,前面更靠近照相机,如果我们开启了深度写入,意味着正方体前面的片元深度深度总是小于背面的深度值,渲染过程中后面的面将永远处于被剔除状态,永远都看不到。但这违背了“半透明物体”的视觉效果了!
2.2 渲染顺序的重要性
场景中物体的渲染顺序十分重要,因为将影响着最终画面上物体之间的遮挡关系是否正确。
对于不透明物体来说,深度缓冲已经为我们解决了渲染顺序的问题。而对于关闭了深度写入的透明度混合操作,渲染顺序就变得不可控了。无论是对于同时渲染两个半透明物体,还是半透明和不透明物体一起渲染,不正确的渲染顺序得到的画面将会非常奇怪。可见,关闭深度写入造成了多么大的混乱!
3 实现透明度测试
3.1 效果展示
可以发现,效果是有锯齿的,不是特别圆滑。
3.2 完整代码
Shader "Unity Shaders Book/Chapter 8/AlphaTest"
{
Properties {
_Color ("Color Tint", Color) = (1, 1, 1, 1)
_MainTex ("Main Tex", 2D) = "white" {}
_Cutoff ("Alpha Cutoff", Range(0, 1)) = 0.5 //这是clip判断条件
}
SubShader {
//透明度测试都应该包含的三个标签
Tags { "Queue"="AlphaTest" "IgnoreProjector"="True" "RenderType"="TransparentCutout" }
Pass {
Tags { "LightMode"="ForwardBase" }
// 是否开启双面渲染的效果
// Cull off
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "Lighting.cginc"
//Properties
fixed4 _Color;
sampler2D _MainTex;
float4 _MainTex_ST;
float _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 lightDir = normalize(UnityWorldSpaceLightDir(i.worldPos));
fixed4 texColor = tex2D(_MainTex, i.uv);
//alpha test
clip(texColor.a - _Cutoff);
//equal to:
//if((texColor.a - _Cutoff) < 0.0){
// discard;
// }
//half lambert:
float halfLambert = saturate(dot(worldNormal, lightDir)) * 0.5 + 0.5;
fixed3 albedo = texColor.rgb * _Color.rgb;
fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.rgb * albedo;
fixed3 diffuse = _LightColor0.rgb * albedo * halfLambert;
return fixed4(ambient + diffuse, 1.0);
}
ENDCG
}
}
FallBack "Transparent/Cutout/VertexLit"
}
3.3 重要内容解析
3.3.1 Tags1:Queue
跟之前进行的着色相比,除了给Pass设置标签,透明度测试会额外给SubShader也设置标签,Queue就是其中之一。为了严格要求上述提到过的渲染顺序问题,Unity提供了渲染队列(render queue),也就是给每个SubShader提供一个可以提前定义渲染队列的机会,这个机会就是Queue这个Tag了,每个渲染队列还有对应的队列索引号,索引号越小越早被渲染。
ShaderLab的Queue标签提供的5个渲染队列
队列名称 | 队列索引号 | 解释 |
Background | 1000 | 最先渲染,通常使用这个队列来渲染那些需要绘制在背景上的物体 |
Geometry | 2000 | 默认的渲染队列,大多数物体(不透明物体)使用这个队列 |
AlphaTest | 2450 | 需要透明度测试的物体使用这个队列,在所有不透明物体后再渲染 |
Transparent | 3000 | 该队列中的物体会按从后往前的顺序进行渲染,使用了透明度混合的物体都应使用这个队列 |
Overlay | 4000 | 使用该队列实现一些叠加效果,最后渲染的物体都应使用这个队列 |
3.3.2 Tags2:IgnoreProjector
标签设置为“True”,意味着使用当前SubShader的物体不会受Projector的影响,这样的设置通常用于半透明物体:
Tags {"IgnoreProjector"="True"}
3.3.3 Tags3:RenderType
该标签可以让Unity把当前Shader归入到提前定义的组,其实就是对着色器进行一个分类,例如当前Shader是个不透明着色器、当前Shader是个透明的着色器等等:
//不透明
Tags {"RenderType"="Opaque"}
//透明度测试
Tags {"RenderType"="TransparentCutout"}
4 实现透明度混合(关闭深度写入)
4.1 效果展示
4.2 完整代码
Shader "Unity Shaders Book/Chapter 8/Alpha Blend"
{
Properties
{
_Color ("Color", Color) = (1, 1, 1, 1)
_MainTex ("Texture", 2D) = "white" {}
_AlphaScale ("Alpha Scale", Range(0, 1)) = 1 //控制整体的透明度
}
SubShader
{
//标签需要设置:Queue, RenderType, IgnoreProjector
Tags { "Queue"="Transparent" "IgnoreProjector"="True" "RenderType"="Transparent" }
Pass
{
Tags { "LightMode"="ForwardBase" }
ZWrite Off
Blend SrcAlpha OneMinusSrcAlpha
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "Lighting.cginc"
//properties
fixed4 _Color;
sampler2D _MainTex;
float4 _MainTex_ST;
fixed _AlphaScale;
struct a2v{
float4 vertex : POSITION;
float4 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);
float halfLambert = saturate(dot(worldNormal, worldLightDir)) * 0.5 + 0.5;
fixed3 albedo = texColor.rgb * _Color.rgb;
fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz * albedo;
fixed3 diffuse = _LightColor0.rgb * albedo * halfLambert;
return fixed4(ambient + diffuse, texColor.a * _AlphaScale);
}
ENDCG
}
}
}
4.3 重要内容解析
我认为,透明度混合的重点在于关闭深度写入和进行源颜色与目标颜色混合这两个步骤上,这就涉及到了之前【Unity Shader】初识Shader,基础总结!中提到的ShaderLab的ZWrite和Blend渲染状态设置命令。
4.3.1 不能忽视的3个标签
与透明度测试一样,透明度混合也为SubShader设置三个标签,
- Queue——Transparent
- IgnoreProjector——True
- RenderType——Transparent
4.3.2 ZWrite命令
ShaderLab的ZWrite设置命令
命令语义 | 解释 |
ZWrite On | 打开深度写入 |
ZWrite Off | 关闭深度写入 |
代码中就体现在了Pass中进行的混合状态设置:
Pass {
...
ZWrite Off
...
}
4.3.3 Blend命令
对于Blend命令,须知:
- 混合时的源颜色是片元着色器计算得到的颜色,目标颜色是颜色缓冲器中当前储存的颜色值;
- 使用Blend命令设置混合因子后,将自动开启混合模式。
ShaderLab的Blend设置命令
命令语义 | 解释 |
Blend Off | 关闭混合 |
Blend SrcFactor DstFactor | 开启混合,并设置混合因子。源颜色*SrcFactor,目标颜色*DstFactor,二者相加后再存入颜色缓冲器中 |
Blend SrcFactor DstFactor, SrcFactorA, DstFactorA | 同上,但是使用不同的因子来混合颜色(RGB)和透明度(A) |
BlendOp Op | 不将混合颜色相加,而是执行不同的操作(Op) |
BlendOp OpColor, OpAlpha | 同上,但是对颜色 (RGB) 通道和 Alpha (A) 通道使用不同的混合操作 |
代码中我们使用了
Blend SrcAlpha OneMinusSrcAlpha
其中这个混合命令,意味着混合后储存到颜色缓冲器中的新目标颜色值将会是:
4.3.4 混合运算&混合系数
上面用到了一个OneMinusSrcAlpha,也就是“1-源颜色”,这里就涉及到了Unity中混合的运算系数。ShaderLab:混合 - Unity 手册中有为我们列举一系列的混合系数:
同时还列举出了常见的混合类型,供我们参考并直接使用:
5 实现透明度混合(开启深度写入)
5.1 关闭深度写入的问题
由于关闭了深度写入,我们就只能纯粹用Unity提供的渲染队列来解决渲染顺序这个问题。但是当遇到渲染模型本身遮挡效果复杂的情况下,效果往往会出错,比如《入门精要》中演示的Knot模型的例子,我还是用了之前透明度混合的Shader,得到的效果肯定是错误的:
这种情况下,就要重新启用深度写入才能实现正确的遮挡效果。
5.2 效果展示
5.3 完整代码
Shader "Unity Shaders Book/Chapter 8/AlphaBlendZWrite"
{
Properties
{
_Color ("Color", Color) = (1, 1, 1, 1)
_MainTex ("Texture", 2D) = "white" {}
_AlphaScale ("Alpha Scale", Range(0, 1)) = 1 //控制整体的透明度
}
SubShader
{
//标签需要设置:Queue, RenderType, IgnoreProjector
Tags { "Queue"="Transparent" "IgnoreProjector"="True" "RenderType"="Transparent" }
//extra pass that renders to depth buffer only
Pass
{
ZWrite On
ColorMask 0
}
Pass
{
Tags { "LightMode"="ForwardBase" }
ZWrite Off
Blend SrcAlpha OneMinusSrcAlpha
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "Lighting.cginc"
//properties
fixed4 _Color;
sampler2D _MainTex;
float4 _MainTex_ST;
fixed _AlphaScale;
struct a2v{
float4 vertex : POSITION;
float4 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);
float halfLambert = saturate(dot(worldNormal, worldLightDir)) * 0.5 + 0.5;
fixed3 albedo = texColor.rgb * _Color.rgb;
fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz * albedo;
fixed3 diffuse = _LightColor0.rgb * albedo * halfLambert;
return fixed4(ambient + diffuse, texColor.a * _AlphaScale);
}
ENDCG
}
}
}
5.4 如何使用两个Pass?
又想达到正确的半透明效果,又想实现正确的遮挡关系,那当然二者要分开进行了!于是第一次接触到了两个Pass!但第一个用于实现正确遮挡关系的Pass很简单:
首先,打开了深度写入,
ZWrite On
接着,ShaderLab 命令:ColorMask用于设置颜色通道的写入遮罩,防止GPU写入渲染目标中的通道,把ColorMask的值设为0,意味着不会写入任何颜色。
ColorMask 0
6 双面渲染的透明效果
之前实现的无论是透明度测试还是透明度混合,都是单面渲染,即只渲染了物体正面的效果(剔除了相对于摄像机是背面的方向),可是现实中的透明物体我们都是能看到正反面的!
是否剔除背对摄像机的渲染图元,Unity中是由Cull指令决定的,Cull指令有三个选项设置:
- Cull Back——不渲染背面图元,这也是默认状态下的剔除状态
- Cull Front——朝向摄像机的图元不会被渲染
- Cull Off——关闭剔除功能,所有的图元都会被渲染
引擎为了节省性能,一般情况下会一直开启剔除功能,如果设置为Off的话,渲染图元数量会成倍的增加!因此除非是想达到双面渲染的效果,一般是不会关闭剔除功能的。
6.1 透明度测试的双面效果
透明度测试实现双面渲染很简单,只需要关闭剔除即可!
//Turn off culling
Cull Off
在Alpha Scale相同的情况下,双面(上)和单面(下)效果对比看看:
6.2 透明度混合的双面效果
双面效果就相对复杂一些,需要写满两个Pass,一个用于前面,一个用于后面,Shader代码如下:
Shader "Unity Shaders Book/Chapter 8/AlphaBlendBothSide"
{
Properties
{
_Color ("Color", Color) = (1, 1, 1, 1)
_MainTex ("Texture", 2D) = "white" {}
_AlphaScale ("Alpha Scale", Range(0, 1)) = 1 //控制整体的透明度
}
SubShader
{
//标签需要设置:Queue, RenderType, IgnoreProjector
Tags { "Queue"="Transparent" "IgnoreProjector"="True" "RenderType"="Transparent" }
//extra pass that renders to depth buffer only
Pass
{
Tags { "LightMode"="ForwardBase"}
//front
Cull Front
ZWrite Off
Blend SrcAlpha OneMinusSrcAlpha
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "Lighting.cginc"
//properties
fixed4 _Color;
sampler2D _MainTex;
float4 _MainTex_ST;
fixed _AlphaScale;
struct a2v{
float4 vertex : POSITION;
float4 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);
float halfLambert = saturate(dot(worldNormal, worldLightDir)) * 0.5 + 0.5;
fixed3 albedo = texColor.rgb * _Color.rgb;
fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz * albedo;
fixed3 diffuse = _LightColor0.rgb * albedo * halfLambert;
return fixed4(ambient + diffuse, texColor.a * _AlphaScale);
}
ENDCG
}
Pass
{
Tags { "LightMode"="ForwardBase" }
//back
Cull Back
ZWrite Off
Blend SrcAlpha OneMinusSrcAlpha
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "Lighting.cginc"
//properties
fixed4 _Color;
sampler2D _MainTex;
float4 _MainTex_ST;
fixed _AlphaScale;
struct a2v{
float4 vertex : POSITION;
float4 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);
float halfLambert = saturate(dot(worldNormal, worldLightDir)) * 0.5 + 0.5;
fixed3 albedo = texColor.rgb * _Color.rgb;
fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz * albedo;
fixed3 diffuse = _LightColor0.rgb * albedo * halfLambert;
return fixed4(ambient + diffuse, texColor.a * _AlphaScale);
}
ENDCG
}
}
FallBack "Transparent/VertexLit"
}
同样看看对比:在Alpha Scale相同的情况下,双面(上)和单面(下)效果: