文章目录
- 前言
- 一、Unity中的渲染顺序
- 二、透明度测试
- 三、透明度混合
- 三、开启深度写入的半透明效果
- 四、双面渲染的透明效果
- 五、补充:常见的混合操作
前言
上一章中主要学习了,在Unity shader中各种纹理的应用。本章主要学习Unity中的透明效果是如何实现的。包括透明度测试和透明度混合。
一、Unity中的渲染顺序
对于不透明(Opaque)的物体,由于深度缓冲(z-buffer)的存在。即使不考虑他们的渲染顺序也能得到正确的渲染关系。
深度缓冲根据深度缓存中的值来判断该片元距离摄像机的距离,以决定哪个物体的哪些部分会被渲染在前面,而哪些部分会被其他物体遮挡 。
但是渲染不透明的物体,就没有那么简单了。
这是因为当我们使用透明度混合时,关闭了深度写入。
为啥要关闭深度写入呢?
假设我们需要渲染半透明物体 A 和不透明物体 B,并且开启深度写入。
第一种情况先渲染 B,再渲染 A:
这种情况下 B首先写入颜色缓冲和深度缓冲。当渲染 A时,发现 A离摄像机更近,通过深度测试。
然后使用 A的透明度与 B的颜色进行混合,这种情况得到正确的半透明效果。
第二种情况先渲染 A ,再渲染 B:
这种情况下 A首先写入颜色缓冲和深度缓冲。当渲染 B时,发现 A离摄像机更近,不能通过深度测试。
这时候B的片元就被舍弃了,没有得到正确的半透明效果。
为了防止这种情况,渲染通常遵循以下规则:
1)先渲染所有不透明物体,并开启它们的深度测试和深度写入。
2)把半透明物体按它们距离摄像机的远近进行排序,然后按照从后往前的顺序渲染这些半透明物体,并开启它们的深度测试,但关闭深度写入。
3)由于当两个网格覆盖的时候,无法得到正确的遮挡关系,此时需要进行网格分割。或者将复杂的模型拆分成可以独立排序的多个子模型。
4)如果不想分割网格,可以尝试让透明通道更加柔和,使穿插看起来不那么明显。
Unity 为了解决渲染顺序的问题提供了渲染队列 (render queue) 这一解决方案。
我们可以使用SubShader Queue 标签来决定我们的模型将归于哪个渲染队列。
unity提前定义的5个渲染队列
名称 | 队列索引号 | 描述 |
Background | 1000 | 这个渲染队列会在任何其他队列之前被渲染,我们通常使用该队列来渲染那些需要绘制在背景上的物体 |
Geometry | 2000 | 默认的渲染队列,大多数物体都使用这个队列。不透明物体使用这个队列 |
AlphaTest | 2450 | 需要透明度测试的物体使用这个队列。在 Unityu5 中它从 Geometry 队列中被单独分出来,这是因为在所有不透明物体渲染之后再渲染它们会更加高效 |
Transparent | 3000 | 这个队列中的物体会在所有 Geometry 和 Alpha Test 物体涫染后,再按从后往前的顺序进行渲染。任何使用了透明度混合(例如关闭了深度写入的 Shader 的物体都应该使用该队列 |
Overlay | 4000 | 该队列用于实现 些叠加效果。任何需要在最后渲染的物体都应该使用该队列 |
如果我们想要通过透明度测试实现透明效果:
SubShader {
Tags { "RenderType"="AlphaTest" }
Pass {...}
}
如果我们想要通过透明度混合来实现透明效果:
SubShader {
Tags { "RenderType"="Transparent" }
Pass
{
// 也可以写在SubShader中对所有Pass都生效
ZWri Off
...
}
}
二、透明度测试
透明度测试是一个很极端的做法:通过测试的按照不透明物处理,未通过测试的片元直接舍弃,
不做任何处理,表现为透明。
clip函数通过discard 指令剔除透明度小于某个阈值的片元
// Upgrade NOTE: replaced '_Object2World' with 'unity_ObjectToWorld'
// Upgrade NOTE: replaced 'mul(UNITY_MATRIX_MVP,*)' with 'UnityObjectToClipPos(*)'
Shader "Unity Shaders Book/Chapter 8/Alpha Test" {
Properties {
_Color ("Color Tint", Color) = (1, 1, 1, 1)
_MainTex ("Main Tex", 2D) = "white" {}
_Cutoff ("Alpha Cutoff", Range(0, 1)) = 0.5
}
SubShader {
//使用了透明度测试的都应该设置这三个标签
//RenderType 标签可以让 Unity 把这个 Shader 归入到提前定义的组(这里就是 TransparentCutout 组)中
//,以指明该 Shader是一个使用了透明度测试的 Shader
// IgnoreProjector 设置为 True, 这意味着这个 Shader 不会受到投影器 (Projectors) 的影响
Tags {"Queue"="AlphaTest" "IgnoreProjector"="True" "RenderType"="TransparentCutout"}
Pass {
Tags { "LightMode"="ForwardBase" }
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "Lighting.cginc"
fixed4 _Color;
sampler2D _MainTex;
float4 _MainTex_ST;
fixed _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 worldLightDir = 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;
//}
fixed3 albedo = texColor.rgb * _Color.rgb;
fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz * albedo;
fixed3 diffuse = _LightColor0.rgb * albedo * max(0, dot(worldNormal, worldLightDir));
return fixed4(ambient + diffuse, 1.0);
}
ENDCG
}
}
FallBack "Transparent/Cutout/VertexLit"
}
三、透明度混合
使用当前片元的透明度作为混合因子,与已经存储在颜色缓冲中的颜色值进行混合,得到新的颜色。透明度混合需要关闭深度写入 ,这使得我们要非常小心物体的渲染顺序。
混合是一个逐片元的操作,高度可配置。
Unity为我们提供了混合语义Blend:
Blend Off:关闭混合
Blend SrcFactor DstFactor :开启混合,并设置混合因子。使用源颜色(该片元产生的颜色)乘以SrcFactor加上目标颜色(已经存在于颜色缓存的颜色)乘以DstFactor来更新颜色缓冲区。
Blend SrcFactor DstFactor, SrcFactorA DstFactorA:使用不同的混合因子来混合透明通道。
BlendOp BlendOperation:使用BlendOperation对源颜色和目标色进行混合。
// Upgrade NOTE: replaced '_Object2World' with 'unity_ObjectToWorld'
// Upgrade NOTE: replaced 'mul(UNITY_MATRIX_MVP,*)' with 'UnityObjectToClipPos(*)'
Shader "Unity Shaders Book/Chapter 8/Alpha Blend" {
Properties {
_Color ("Color Tint", Color) = (1, 1, 1, 1)
_MainTex ("Main Tex", 2D) = "white" {}
_AlphaScale ("Alpha Scale", Range(0, 1)) = 1
}
SubShader {
//使用了透明度混合的Shader都应该设置这三个标签
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"
fixed4 _Color;
sampler2D _MainTex;
float4 _MainTex_ST;
fixed _AlphaScale;
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 worldLightDir = normalize(UnityWorldSpaceLightDir(i.worldPos));
fixed4 texColor = tex2D(_MainTex, i.uv);
fixed3 albedo = texColor.rgb * _Color.rgb;
fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz * albedo;
fixed3 diffuse = _LightColor0.rgb * albedo * max(0, dot(worldNormal, worldLightDir));
return fixed4(ambient + diffuse, texColor.a * _AlphaScale);
}
ENDCG
}
}
FallBack "Transparent/VertexLit"
}
三、开启深度写入的半透明效果
当模型本身有复杂的遮挡关系或是包含了复杂的非凸网格的时候,就会有各种各样因为排序错误而产生的错误的透明效果。由于我们关闭了深度写入,这样我们就无法对模型进行像素级别的深度排序。
而分割网格在大多数情况下是不切实际的。
为了得到该模型正确的遮挡关系,我们可以使用两个pass来渲染该模型。
在第一个pass中开启深度写入,但不输出颜色,仅仅是为了把该模型的深度值写入深度缓冲中。
这样我们在第二个pass进行正常的透明度混合,由于上第一个Pass得到了逐像素的正确的深度信息,该 Pass 就可以按照像素级别的深度排序结果进行透明渲染。
但种方法的缺点在于,多使用一个 Pass 会对性能造成一定的影响。
而且模型内部之间不会有任何真正的半透明效果。
// Upgrade NOTE: replaced '_Object2World' with 'unity_ObjectToWorld'
// Upgrade NOTE: replaced 'mul(UNITY_MATRIX_MVP,*)' with 'UnityObjectToClipPos(*)'
Shader "Unity Shaders Book/Chapter 8/Alpha Blending With ZWrite" {
Properties {
_Color ("Color Tint", Color) = (1, 1, 1, 1)
_MainTex ("Main Tex", 2D) = "white" {}
_AlphaScale ("Alpha Scale", Range(0, 1)) = 1
}
SubShader {
Tags {"Queue"="Transparent" "IgnoreProjector"="True" "RenderType"="Transparent"}
// Extra pass that renders to depth buffer only
Pass {
ZWrite On
//ColorMask 设为 0 时,意味着该 Pass 不写入任何颜色通道,即不会输出任何颜色
ColorMask 0
}
Pass {
Tags { "LightMode"="ForwardBase" }
ZWrite Off
Blend SrcAlpha OneMinusSrcAlpha
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "Lighting.cginc"
fixed4 _Color;
sampler2D _MainTex;
float4 _MainTex_ST;
fixed _AlphaScale;
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 worldLightDir = normalize(UnityWorldSpaceLightDir(i.worldPos));
fixed4 texColor = tex2D(_MainTex, i.uv);
fixed3 albedo = texColor.rgb * _Color.rgb;
fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz * albedo;
fixed3 diffuse = _LightColor0.rgb * albedo * max(0, dot(worldNormal, worldLightDir));
return fixed4(ambient + diffuse, texColor.a * _AlphaScale);
}
ENDCG
}
}
FallBack "Transparent/VertexLit"
}
四、双面渲染的透明效果
在前面实现的透明效果中 无论是透明度测试还是透明度混合,我们都无法观察到正方体内部及其背面的形状,导致物体看起来就好像只有半个。
这是因为默认情况下渲染引擎剔除了物体背面(相对于摄像机的方向)的渲染图元,而只渲染了物体的正面。如果我们想要得到双面渲染的效果,可以使用 Cull 指令来控制需要剔除哪个面的渲染图元。
Cull Back I Front I Off // 剔除背面 | 正面 | 关闭剔除
对于透明度测试,我们只需要使用 Cull Off 关闭剔除,就可以得到双面渲染的效果。
对于透明度混合,由于关闭了深度写入,我们无法保证图元是从后往前渲染的。
因此我们需要分别使用两个Pass进行渲染。第一个Pass渲染背面,第二个Pass渲染正面。
这是因为Unity 会顺序执行 SubShader 中的各个 Pass,因此我们可以保证背面总是在正面被渲染之前渲染,从而可以保证正确的深度渲染关系。
Pass {
Cull Front
// 和之前一样的代码
}
Pass {
Cull Back
// 和之前一样的代码
}
可以看到,实现了双面渲染的透明效果可以看到模型的内部结构。
五、补充:常见的混合操作
前面我们提到,使用 Blend 加混合因子的形式进行混合。
混合的时候会使用两个混合等式分别对源颜色(当前片元颜色)和目标颜色(颜色缓冲区颜色)的RGB通道和A通道进行混合。因此我们需要4个混合因子。
当只指定两个混合因子时,对RGB通道和A通道使用相同的混合因子进行混合。
ShaderLab中的混合因子:
参数 | 描述 |
One | 因子为1 |
Zero | 因子为0 |
SrcColor | 因子为源颜色值。 当用于混合RGB的混合等式时, 使用SrcColor的RGB分量作为混合因子;当用于混合A的混合等式时,使用SrcColor的A分量作为混合因子。 |
SrcAlpha | 因子为源颜色值的A通道值。 |
DstColor | 因子为目标颜色值。 当用于混合RGB的混合等式时, 使用DstColor 的RGB分量作为混合因子;当用于混合A的混合等式时,使用DstColor 的A分量作为混合因子。 |
DstAlpha | 因子为目标颜色值的A通道值。 |
OneMinusSrcColor | 因子为(1 - 源颜色),当用于混合RGB的混合等式时, 使用结果的RGB分量作为混合因子;当用于混合A的混合等式时,使用结果的A分量作为混合因子。 |
OneMinusSrcAlpha | 因子为(1 - 源颜色)的A通道值 |
OneMinusDstColor | 因子为(1 - 目标颜色),当用于混合RGB的混合等式时, 使用结果的RGB分量作为混合因子;当用于混合A的混合等式时,使用结果的A分量作为混合因子。 |
OneMinusDstAlpha | 因子为(1 - 目标颜色)的A通道值 |
使用混合因子对源颜色和目标颜色混合后,需要进行混合操作。默认为相加。
ShaderLab中的混合操作:
参数 | 描述 |
Add | 将混合后的源颜色和目标颜色相加。 默认的混合操作。 |
Sub | 用混合后的源颜色减去混合后的目标颜色。 |
RevSub | 用混合后的目标颜色减去混合后的源颜色。 |
Min | 使用源颜色和目标颜色中较小的值, 是逐分量比较的。 |
Max | 使用源颜色和目标颜色中较大的值, 是逐分量比较的。 |
得到的效果如下:
注意:虽然上面使用 Min Max 混合操作时仍然设置了混合因子,但实际上它们不会对结果有任何影响,因为 Min Max 混合操作会忽略混合因子。