《Unity Shader 入门精要》总结04
《Unity Shader 入门精要》学习总结04
透明渲染
需要在实时渲染中实现透明效果,通常会在渲染模型时控制透明通道,并开启透明混合,使其与之前的渲染结果进行混合,而不是直接覆盖。
在 Unity 中实现透明效果,第一种是透明度测试(Alpha Test),还有一种是透明度混合(Alpha Blending)。
- 透明度测试:判断片元透明度是否满足条件,若是不满足,直接舍弃;满足则使用不透明物体的处理方式,不需要关闭深度写入,是一种开销小的方式。抖动透明就是基于透明度测试实现的。
- 透明度混合:真正的半透明效果,开启后会使用当前片元的透明度作为混合因子,与颜色缓冲中的颜色进行混合,得到新颜色。在使用透明度混合时,我们需要关闭深度写入,也就是说,对于透明度混合渲染来说,深度数据是只读的。此外我们还需要小心处理渲染顺序。
渲染顺序
渲染顺序在透明度混合渲染中是一个大难点。因为透明度混合渲染需要关闭深度写入,还需要正确的顺序进行混合。如果我们没有关闭深度写入,会导致特定渲染顺序下,后面的片元被剔除。但是,在关闭深度写入后,我们需要格外注意渲染顺序。可以看看下面的例子。

前置设定
| 左图 | 右图 | |
|---|---|---|
| A(近) | 透明 α | 透明 α |
| B(远) | 不透明 | 透明 β |
| 背景 G | 已渲染(颜色 g) | 已渲染(颜色 g) |
| 深度测试 | 始终开启 | 始终开启 |
| 深度写入 | A 关闭,B(不透明)开启 | 两个均关闭 或 开启后做对比 |
| 混合算子 | α·src + (1-α)·dst |
同左 |
左图情况 1:先 B 后 A
- B(不透明)先渲染:颜色缓冲 =
b,深度缓冲 = B 的深度。 - A(透明)后渲染:深度测试通过(A 更近),关闭深度写入。
混合: 得到 A 叠加在 B 上的正确半透明效果。
左图情况 2:先 A 后 B
- 背景 G:颜色缓冲 =
g,深度缓冲 = ∞。 - A(透明)先渲染:深度测试通过,关闭深度写入(深度缓冲仍为 ∞)。
混合: - B(不透明)后渲染:深度测试通过(B 的深度 < ∞),B 直接覆盖。
A 完全消失。
右图情况 1:先 B 后 A
- 背景 G:
C_buf = g。 - B(透明)渲染:
- A(透明)渲染:
正确:A 的 α 叠加在"B+背景"之上。
右图情况 2:先 A 后 B
- 背景 G:
C_buf = g。 - A(透明)先渲染:
- B(透明)后渲染:深度测试通过(深度写入关闭,或者深度缓冲区没有 A 的记录),混合:
错误:B 的 β 现在作用在"A+背景"上,视觉上 B 浮到了 A 前面。
若深度写入开启:A 先写深度 → B 深度测试失败 → B 被丢弃,只剩 A+背景,也是错误。
透明混合的 over 算子中 α 只属于"源"片元。交换渲染顺序就交换了"谁是源",于是 α 和 β 作用的对象完全改变。关闭深度写入防止了错误排序时片元被完全丢弃,但正确的从远到近排序仍是获得正确半透明画面的唯一途径。
从上面的例子看出,不管是半透明和不透明混合,还是两个半透明混合,渲染顺序都非常重要。
所以,Unity 在渲染物体时,会对物体进行排序,再按照顺序进行渲染,其常规逻辑是:
- 先渲染所有不透明物体
- 再以从后往前的顺序渲染所有的透明物体
但是纵使按照这样的渲染逻辑来渲染,依旧会产生错误的混合渲染结果。这个错误便是源于我们无法精确地给出半透明物体的渲染排序。
在 Unity 中,可能两个物体不是完全的一前一后,有可能是相互交织,或是存在有复杂的遮挡关系。这时,这套基于粗粒度(以物体为排序单位)的排序逻辑就彻底失效了。

遇到这种情况,我们只能尝试对模型进行拆分,多使用凸面体,或是使用不会受到排序影响的混合算法。
Unity Shader 中的渲染顺序
前面说到,Unity 默认管线会按照一个队列排序渲染的物体。Unity 使用了一个叫做 渲染队列(render queue)的解决方案。我们可以在 SubShader 的 Queue 标签中设置模型的渲染队列和顺序(当然在材质面板中也有这个设置项,会覆盖 SubShader 中的标签设置)。

透明度测试在 Unity Shader 中的实现
我们会在 shader 代码块中使用 clip 函数进行透明度测试。clip 是一个 CG 函数。
函数声明: void clip(float4 x) / void clip(float3 x) / void clip(float2 x) / void clip(float x)
参数: 裁剪时使用的标量或矢量条件。
描述: 如果给定参数的任何一个分量是负数,舍弃当前像素的输出颜色。
float是 HLSL 中的标量类型,在着色器代码中统一使用float即可,无需刻意写作float1。
代码如下:
Shader "Unity入门精要/Chapter8/AlphaTest"
{
Properties
{
_MainTex ("颜色贴图", 2D) = "white" {}
_Color ("基础色", Color) = (0.0, 0.0 ,0.0, 0.0)
_Clip ("裁切阈值", Range(0, 1)) = 0.0
}
SubShader
{
Tags { "RenderType"="TransparentCutout" "Queue" = "AlphaTest" "IgnoreProjector" = "True" }
Pass
{
Tags { "LightMode" = "ForwardBase" }
Cull Off
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
#include "Lighting.cginc"
struct appdata
{
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
float3 normal : NORMAL;
};
struct v2f
{
float2 uv : TEXCOORD0;
float4 vertex : SV_POSITION;
float4 vertex_WS : TEXCOORD1;
float3 normal_WS : TEXCOORD2;
};
sampler2D _MainTex;
float4 _MainTex_ST;
float4 _Color;
fixed _Clip;
v2f vert (appdata input)
{
v2f output;
output.vertex = UnityObjectToClipPos(input.vertex);
output.uv = input.uv;
output.vertex_WS = mul(unity_ObjectToWorld, input.vertex);
output.normal_WS = UnityObjectToWorldNormal(input.normal);
return output;
}
fixed4 frag (v2f input) : SV_Target
{
fixed3 normal_WS = normalize(input.normal_WS);
fixed3 lightDir_WS = normalize(UnityWorldSpaceLightDir(input.vertex_WS));
fixed NdotL = dot(normal_WS , lightDir_WS);
fixed4 mainTex = tex2D(_MainTex, input.uv * _MainTex_ST.xy + _MainTex_ST.zw);
fixed3 albedo = _Color * mainTex.xyz;
fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz * albedo;
fixed3 diffuse = _LightColor0 * albedo * max(0, NdotL);
clip(mainTex.a - _Clip); // 使用 clip 函数进行透明度检测裁切
return fixed4(diffuse + ambient, 1.0);
}
ENDCG
}
}
Fallback "Transparent/Cutout/VertexLit"
}
透明度混合在 Unity Shader 中的实现
为了进行透明度混合,我们需要使用 Unity 提供的混合命令——Blend。只有开启混合时,片元的 Alpha 通道才有意义。

下面会使用第二种语义进行混合。第二种语义会以下面方式进行颜色混合:
代码如下。
单面透明渲染:
Shader "Unity入门精要/Chapter8/AlphaBlend"
{
Properties
{
_MainTex ("颜色贴图", 2D) = "white" {}
_Color ("基础色", Color) = (0.0, 0.0 ,0.0, 0.0)
_AlphaScale ("Alpha偏移", Range(0, 1)) = 1.0
}
SubShader
{
Tags { "RenderType"="Transparent" "Queue" = "Transparent" "IgnoreProjector" = "True" }
Pass
{
Tags { "LightMode" = "ForwardBase" }
ZWrite Off
Blend SrcAlpha OneMinusSrcAlpha
// Blend SrcAlpha Zero
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
#include "Lighting.cginc"
struct appdata
{
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
float3 normal : NORMAL;
};
struct v2f
{
float2 uv : TEXCOORD0;
float4 vertex : SV_POSITION;
float4 vertex_WS : TEXCOORD1;
float3 normal_WS : TEXCOORD2;
};
sampler2D _MainTex;
float4 _MainTex_ST;
float4 _Color;
fixed _AlphaScale;
v2f vert (appdata input)
{
v2f output;
output.vertex = UnityObjectToClipPos(input.vertex);
output.uv = input.uv;
output.vertex_WS = mul(unity_ObjectToWorld, input.vertex);
output.normal_WS = UnityObjectToWorldNormal(input.normal);
return output;
}
fixed4 frag (v2f input) : SV_Target
{
fixed3 normal_WS = normalize(input.normal_WS);
fixed3 lightDir_WS = normalize(UnityWorldSpaceLightDir(input.vertex_WS));
fixed NdotL = dot(normal_WS , lightDir_WS);
fixed4 mainTex = tex2D(_MainTex, input.uv * _MainTex_ST.xy + _MainTex_ST.zw);
fixed3 albedo = _Color * mainTex.xyz;
fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz * albedo;
fixed3 diffuse = _LightColor0 * albedo * max(0, NdotL);
fixed alpha = mainTex.a * _AlphaScale;
return fixed4(diffuse + ambient, alpha);
}
ENDCG
}
}
Fallback "Transparent/VertexLit"
}
双面透明渲染:
使用 Cull 指令,两个 Pass 分别渲染背面和正面。
Shader "Unity入门精要/Chapter8/AlphaBlendBS"
{
Properties
{
_MainTex ("颜色贴图", 2D) = "white" {}
_Color ("基础色", Color) = (0.0, 0.0 ,0.0, 0.0)
_AlphaScale ("Alpha偏移", Range(0, 1)) = 1.0
}
SubShader
{
Tags { "RenderType"="Transparent" "Queue" = "Transparent" "IgnoreProjector" = "True" }
// Pass 1:渲染背面
Pass
{
Tags { "LightMode" = "ForwardBase" }
ZWrite Off
Cull Front
Blend SrcAlpha OneMinusSrcAlpha
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
#include "Lighting.cginc"
struct appdata
{
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
float3 normal : NORMAL;
};
struct v2f
{
float2 uv : TEXCOORD0;
float4 vertex : SV_POSITION;
float4 vertex_WS : TEXCOORD1;
float3 normal_WS : TEXCOORD2;
};
sampler2D _MainTex;
float4 _MainTex_ST;
float4 _Color;
fixed _AlphaScale;
v2f vert (appdata input)
{
v2f output;
output.vertex = UnityObjectToClipPos(input.vertex);
output.uv = input.uv;
output.vertex_WS = mul(unity_ObjectToWorld, input.vertex);
output.normal_WS = UnityObjectToWorldNormal(input.normal);
return output;
}
fixed4 frag (v2f input) : SV_Target
{
fixed3 normal_WS = normalize(input.normal_WS);
fixed3 lightDir_WS = normalize(UnityWorldSpaceLightDir(input.vertex_WS));
fixed NdotL = dot(normal_WS , lightDir_WS);
fixed4 mainTex = tex2D(_MainTex, input.uv * _MainTex_ST.xy + _MainTex_ST.zw);
fixed3 albedo = _Color * mainTex.xyz;
fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz * albedo;
fixed3 diffuse = _LightColor0 * albedo * max(0, NdotL);
fixed alpha = mainTex.a * _AlphaScale;
return fixed4(diffuse + ambient, alpha);
}
ENDCG
}
// Pass 2:渲染正面
Pass
{
Tags { "LightMode" = "ForwardBase" }
ZWrite Off
Cull Back
Blend SrcAlpha OneMinusSrcAlpha
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
#include "Lighting.cginc"
struct appdata
{
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
float3 normal : NORMAL;
};
struct v2f
{
float2 uv : TEXCOORD0;
float4 vertex : SV_POSITION;
float4 vertex_WS : TEXCOORD1;
float3 normal_WS : TEXCOORD2;
};
sampler2D _MainTex;
float4 _MainTex_ST;
float4 _Color;
fixed _AlphaScale;
v2f vert (appdata input)
{
v2f output;
output.vertex = UnityObjectToClipPos(input.vertex);
output.uv = input.uv;
output.vertex_WS = mul(unity_ObjectToWorld, input.vertex);
output.normal_WS = UnityObjectToWorldNormal(input.normal);
return output;
}
fixed4 frag (v2f input) : SV_Target
{
fixed3 normal_WS = normalize(input.normal_WS);
fixed3 lightDir_WS = normalize(UnityWorldSpaceLightDir(input.vertex_WS));
fixed NdotL = dot(normal_WS , lightDir_WS);
fixed4 mainTex = tex2D(_MainTex, input.uv * _MainTex_ST.xy + _MainTex_ST.zw);
fixed3 albedo = _Color * mainTex.xyz;
fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz * albedo;
fixed3 diffuse = _LightColor0 * albedo * max(0, NdotL);
fixed alpha = mainTex.a * _AlphaScale;
return fixed4(diffuse + ambient, alpha);
}
ENDCG
}
}
Fallback "Transparent/VertexLit"
}
深度写入的半透明效果:
使用该方法来进行准确的像素级深度排序进行透明渲染,使复杂的模型能够渲染出正确的透明渲染效果,并剔除掉被遮挡部分。
此方法需要注意渲染顺序,否则会产生剔除掉后面物体的现象。
在你的透明混合 Pass 前加上一个这样的 Pass:
Pass
{
ZWrite On
ColorMask 0 // 使用 ColorMask 来关闭色彩缓冲写入
}
这个 Pass 会进行深度预写入,但是不会写入色彩缓冲。
ShaderLab 混合命令
混合和两个操作数有关:
- 源颜色(source color):片元着色器产生的颜色
- 目标颜色(destination color):颜色缓冲中得到的颜色
我们使用混合等式进行混合运算,在混合中使用两个等式,一个用于混合颜色,一个用于混合 Alpha。

例如上表中下面那一行的命令,就是分开计算了 Alpha 混合:
下面是一些 ShaderLab 支持的混合因子:

下面是一些常用的混合操作:


常见的混合类型:





