1. 从“透明”说起:为什么我们需要混合模式
在Unity里做特效或者UI的时候,你肯定遇到过这种情况:想让一个半透明的材质盖在另一个材质上面,比如烟雾、玻璃、水面,或者一个简单的UI淡入淡出效果。你兴冲冲地调好了材质的Alpha通道,结果发现渲染出来的效果要么完全不透明,要么透过去的部分一片漆黑,或者颜色叠加得乱七八糟,完全不是想象中的“通透”感。
这就是我们今天要聊的Shader混合模式(Blend)要解决的问题。它不是一个高深莫测的图形学黑魔法,而是一套非常具体的“配方”,用来告诉GPU:当一个新的片元(可以理解为一个准备画上去的像素点)要覆盖屏幕上已有的颜色时,这两者应该怎么“混合”在一起。没有这个配方,GPU默认的操作就是“覆盖”(也叫“不混合”),新来的颜色直接把旧的颜色替换掉,这就没法实现任何透明、叠加、发光等视觉效果。
所以,理解Blend,本质上就是理解颜色在屏幕上的合成规则。它直接决定了你看到的视觉效果是否正确、是否高效。无论是实现一个简单的按钮点击高亮,还是制作复杂的体积光、粒子特效,都离不开对混合模式的精确控制。如果你曾经对Shader Graph里那个Blend节点感到困惑,或者写Shader代码时对那一行Blend SrcAlpha OneMinusSrcAlpha似懂非懂,那么这篇文章就是为你准备的。我会从最根本的“为什么”讲起,拆解每一个参数的含义,并结合实际开发中踩过的坑,让你彻底掌握这个看似简单却至关重要的功能。
2. 混合的本质:一次简单的数学运算
要理解混合,我们得先抛开Shader,想想现实中的情况。你把一滴红色颜料(源颜色,Source Color)滴在一张已经涂了蓝色颜料(目标颜色,Destination Color)的画布上,会发生什么?如果这滴红色是完全不透明的,它会完全盖住蓝色。如果它是半透明的,你会看到红色和蓝色混合产生了一种新的紫色。GPU的混合过程,就是对这个物理过程的一个数学模拟。
在渲染管线中,混合发生在所有深度测试、模板测试之后,一个片元即将被写入帧缓冲(Frame Buffer)的那一刻。此时,帧缓冲中对应位置已经有一个颜色值了(即目标颜色DstColor),而当前片元计算出的颜色是源颜色SrcColor。混合模式,就是定义了一个等式:
最终颜色 = SrcColor * SrcFactor + DstColor * DstFactor
这个等式的核心就是两个因子:源因子(SrcFactor)和目标因子(DstFactor)。它们不是固定的值,而是可以从一系列预定义选项中选取的“权重”。Unity(或者说底层的图形API,如OpenGL、Direct3D)提供了一系列内置的因子,比如:
One: 因子为1。意味着对应的颜色(SrcColor或DstColor)以原强度参与混合。Zero: 因子为0。意味着对应的颜色完全不参与混合。SrcColor: 因子为源颜色本身的RGB值。这是一个变量因子。SrcAlpha: 因子为源颜色的Alpha值。这是最常用的因子之一。DstColor: 因子为目标颜色的RGB值。DstAlpha: 因子为目标颜色的Alpha值。OneMinusSrcColor: 因子为 (1 - 源颜色RGB)。OneMinusSrcAlpha: 因子为 (1 - 源颜色Alpha)。这同样是透明混合的标配。OneMinusDstColor,OneMinusDstAlpha: 同理。
看到这里,你应该能理解那行经典代码了:Blend SrcAlpha OneMinusSrcAlpha。
SrcFactor是SrcAlpha:源颜色的权重是其自身的Alpha值。Alpha为1(不透明)则完全使用源色,Alpha为0(全透明)则源色权重为0。DstFactor是OneMinusSrcAlpha:目标颜色的权重是 (1 - 源Alpha)。源越不透明,目标颜色的贡献就越小。- 最终颜色 =
SrcColor * SrcAlpha + DstColor * (1 - SrcAlpha)。这就是计算机图形学中最标准的Alpha混合公式,完美模拟了半透明叠加效果。
注意:混合是针对颜色缓冲区(Color Buffer)的操作。深度缓冲区(Depth Buffer)和模板缓冲区(Stencil Buffer)在混合阶段通常已经完成了它们的测试工作(除非开启深度写入控制)。对于半透明物体,我们通常会关闭深度写入(
ZWrite Off),但保持深度测试(ZTest),以避免不透明的物体被错误地渲染在半透明物体后面。
3. Unity Shader中的混合语法详解
在Unity的ShaderLab语法中,我们主要在Pass块内使用Blend指令来声明混合模式。它的写法非常灵活,但核心是围绕上面提到的因子展开的。
3.1 标准混合指令
最常用的形式是分别指定RGB通道和Alpha通道的混合方式,但通常我们用一个指令同时处理它们:
// 语法:Blend <SrcFactor> <DstFactor> Blend SrcAlpha OneMinusSrcAlpha // 最最常用的透明混合这条指令同时设置了颜色(RGB)和透明度(A)的混合因子。对于Alpha通道,其混合公式也是:最终Alpha = SrcAlpha * SrcFactor + DstAlpha * DstFactor。在Blend SrcAlpha OneMinusSrcAlpha这个例子中,Alpha通道的混合因子也是SrcAlpha和OneMinusSrcAlpha。
3.2 分离的混合指令
有时候,我们希望颜色和透明度的混合规则不一样。比如一些特殊的叠加效果,颜色需要加算,但Alpha通道需要保持某种规则。这时可以使用分离指令:
// 语法:Blend <颜色SrcFactor> <颜色DstFactor>, <Alpha SrcFactor> <Alpha DstFactor> Blend SrcAlpha OneMinusSrcAlpha, One Zero这个指令的含义是:
- 颜色混合:
最终颜色RGB = SrcColorRGB * SrcAlpha + DstColorRGB * (1 - SrcAlpha)。 - Alpha混合:
最终Alpha = SrcAlpha * 1 + DstAlpha * 0 = SrcAlpha。也就是说,最终写入的Alpha值完全由源(当前片元)的Alpha决定,忽略目标位置原有的Alpha。这在某些需要严格控制Alpha通道写入的渲染目标(Render Target)时有用。
3.3 预定义的混合状态
除了手动指定因子,Unity还提供了一些预定义的、有特殊名称的混合模式,它们本质上是特定因子组合的快捷方式。在Shader Graph中,你看到的选项就是这些:
Blend Off:关闭混合。等同于Blend One Zero,即完全覆盖。BlendOp Add:这是默认的混合操作,也就是我们一直在用的加法。公式就是Src * SrcFactor + Dst * DstFactor。- 其他模式如
Multiply(正片叠底)、Screen(滤色)等,在Unity Shader代码中通常需要通过BlendOp指令配合Blend因子来实现,但在Shader Graph里被封装成了直观的节点。我们稍后会详细说。
3.4 混合操作(BlendOp)
默认的混合操作是加法(Add)。但混合不仅仅是加法,还可以是减法、取最小/最大值等。这通过BlendOp指令来指定。
BlendOp Sub // 指定混合操作是减法 Blend SrcAlpha One // 此时公式变为:最终颜色 = DstColor * One - SrcColor * SrcAlpha常见的BlendOp有:
Add: 加(默认)。Final = Src * SrcFactor + Dst * DstFactorSub: 减(目标减源)。Final = Dst * DstFactor - Src * SrcFactorRevSub: 反向减(源减目标)。Final = Src * SrcFactor - Dst * DstFactorMin: 取RGB分量最小值。Final = min(Src, Dst)。此时Blend因子被忽略。Max: 取RGB分量最大值。Final = max(Src, Dst)。此时Blend因子被忽略。
Min和Max操作在某些特殊效果,比如实现某些类型的深度融合或者亮度保持时非常有用。
4. 从代码到可视化:Shader Graph中的混合节点
如果你主要使用Shader Graph,那么混合模式的设置会更加直观。你不需要记住那些因子字符串,而是通过节点和下拉菜单来选择。
在Shader Graph的主节点(Master Node,如PBR Master或Unlit Master)上,找到Surface(表面)选项。将Surface Type从默认的Opaque(不透明)改为Transparent(透明)时,下方会自动出现Blend(混合)选项。
这里通常有几个预设:
- Alpha: 对应代码
Blend SrcAlpha OneMinusSrcAlpha。最通用的半透明。 - Premultiply(预乘Alpha): 对应
Blend One OneMinusSrcAlpha。这要求你的输入纹理颜色已经是预乘过Alpha的(即RGB *= Alpha)。这种混合在某些情况下(如粒子特效)能避免颜色边缘出现黑边,性能也稍好。 - Additive(加法/叠加): 对应
Blend One One或Blend SrcAlpha One。源颜色直接加到目标颜色上,实现发光、火焰、光晕等效果。Blend One One是完全的亮度叠加,Blend SrcAlpha One则让源颜色的Alpha控制叠加强度。 - Multiply(乘法/正片叠底): 对应
Blend DstColor Zero或Blend DstColor SrcColor(需要配合BlendOp)。颜色会变暗,模拟阴影、污渍等效果。
在Shader Graph中,你还可以通过Custom(自定义)选项,手动连接因子。Graph提供了Blend节点,你可以将计算好的因子(一个Float或Vector3)连接到SrcFactor和DstFactor端口,实现代码中那样的灵活控制。
一个关键区别:在代码Shader中,混合指令是全局作用于整个Pass的。而在Shader Graph中,混合模式是Master Node的一个属性,意味着同一个Shader Graph资产通常只定义一种混合方式。如果你想在一个Shader中实现多种混合效果(比如一个Pass做Additive,另一个Pass做Alpha Blend),可能需要创建多个Shader Graph变体,或者回退到手写Shader代码。
5. 实战中的混合:常见效果实现与避坑指南
理论说再多,不如动手做一遍。下面我们来看几个具体的应用场景和必须注意的“坑”。
5.1 实现标准半透明(Alpha Blending)
这是最基础的需求。材质球上一个透明的纹理,比如树叶、玻璃窗。
Shader代码实现:
Shader "Custom/SimpleAlphaBlend" { Properties { _MainTex ("Texture", 2D) = "white" {} _Color ("Color Tint", Color) = (1,1,1,1) } SubShader { Tags { "Queue"="Transparent" "RenderType"="Transparent" } LOD 100 // 关键步骤1:关闭深度写入,防止半透明物体阻挡彼此 ZWrite Off // 关键步骤2:开启混合,并设置为标准Alpha混合 Blend SrcAlpha OneMinusSrcAlpha Pass { CGPROGRAM #pragma vertex vert #pragma fragment frag ... // 采样纹理,输出颜色 ENDCG } } }避坑点1:渲染队列(Queue)必须将Tags中的Queue设置为Transparent(或者AlphaTest、Transparent+数字)。Unity的渲染顺序是:先渲染所有不透明物体(Queue<= 2500),再按从后到前的顺序渲染透明物体(Queue> 2500)。如果你的透明物体没有被正确排序,会出现奇怪的遮挡问题。对于复杂的透明物体,有时需要使用"Queue"="Transparent+100"来手动调整渲染顺序。
避坑点2:深度写入(ZWrite)对于大多数真正的半透明物体,我们需要关闭深度写入(ZWrite Off)。为什么?因为深度写入意味着这个物体会更新深度缓冲,后续的物体会被它遮挡。但半透明物体是应该能看到它背后的物体的。如果开启深度写入,一个半透明物体渲染后,它背后的另一个半透明物体就可能因为深度测试失败而被丢弃,无法实现正确的叠加。所以,我们关闭深度写入,但保持深度测试(默认ZTest LEqual),这样它依然会被前面的不透明物体正确遮挡。
5.2 实现加法发光效果(Additive Blending)
用于火焰、粒子、激光、全息投影等需要“增亮”屏幕的效果。
Shader代码实现:
Blend One One // 或者 Blend SrcAlpha OneBlend One One: 最终颜色 = 源颜色 * 1 + 目标颜色 * 1。颜色会线性叠加,非常亮,容易过曝。适合小范围的高光粒子。Blend SrcAlpha One: 最终颜色 = 源颜色 * 源Alpha + 目标颜色 * 1。源颜色的Alpha控制发光强度,叠加到背景上。这是更常用、更可控的加法混合,能产生漂亮的辉光效果,且叠加多次也不会无限变亮(因为源Alpha通常小于1)。
避坑点:叠加顺序与性能加法混合的物体通常也应该放在Transparent队列,并且关闭深度写入。但加法混合有一个特性:它的结果与渲染顺序无关(因为满足交换律,A+B和B+A结果一样)。所以对于纯加法混合(Blend One One)的物体,你不需要太担心它们的渲染顺序。但是,Blend SrcAlpha One由于源Alpha的参与,严格来说与顺序有微弱关系。在实践中,我们通常还是按透明物体处理。
一个重要的性能提示:加法混合因为关闭了深度写入,且通常用于大量粒子,会导致严重的Overdraw(过度绘制)。一个像素可能被多个粒子片元反复绘制多次。这在移动平台是性能杀手。务必严格控制粒子系统的最大粒子数、发射速率,并利用粒子的裁剪(Frustum Culling)和LOD。
5.3 实现软粒子(Soft Particles)
这是一个经典的、必须理解混合的案例。普通粒子在与场景几何体交叉时,边界会显得非常生硬。软粒子通过让粒子在靠近场景表面时逐渐淡出,来模拟一种柔和的融合效果。
其核心原理是:在片元着色器中,除了计算自身的颜色,还要采样当前像素的深度纹理(Camera Depth Texture),获取场景中已渲染物体的深度值。然后与当前粒子的深度值进行比较。如果两者非常接近,就降低当前粒子颜色的Alpha值(甚至降到0),从而实现“接近表面时变透明”。
实现步骤简述:
- 在Shader中声明并采样
_CameraDepthTexture。 - 在顶点着色器中,将顶点位置变换到裁剪空间,并输出给片元着色器。
- 在片元着色器中,将屏幕空间坐标和深度值还原出当前片元在视图空间中的深度。
- 同样,从深度纹理中采样,还原出场景中对应位置物体的视图空间深度。
- 计算两者差值,根据差值(距离)重新计算一个衰减系数,乘到最终的Alpha值上。
// 片元着色器核心逻辑示例 float sceneDepth = LinearEyeDepth(SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, screenUV)); float particleDepth = i.viewPos.z; // 假设i.viewPos是视图空间中的位置 float depthDiff = sceneDepth - particleDepth; float softFactor = saturate(depthDiff / _SoftParticlesDistance); // _SoftParticlesDistance是控制软化范围的参数 alpha *= softFactor;避坑点:深度纹理与精度开启深度纹理需要相机设置Camera.depthTextureMode |= DepthTextureMode.Depth;。在移动平台上,深度纹理的精度(通常是16位或24位)可能不足,在远距离时会产生带状瑕疵(banding)。对于要求不高的软粒子,有时可以用简单的距离淡化(基于粒子中心到相机或表面的距离)来近似,虽然效果差一些,但性能更好。
5.4 UI元素的混合与注意事项
Unity UI(uGUI)的默认材质使用的就是标准的Blend SrcAlpha OneMinusSrcAlpha。但在UI中,混合问题常常以另一种形式出现:Canvas的渲染顺序和Overdraw。
UI元素在同一个Canvas下,其渲染顺序由Hierarchy中的顺序决定(后绘制的在上层)。当你有大量半透明UI重叠时(比如多个半透明面板、窗口),Overdraw会急剧上升。更棘手的是,UI系统默认的合批(Batching)规则可能会因为层级顺序而被打断,导致Draw Call增加。
优化建议:
- 尽可能减少UI的层级和重叠:这是根本。
- 对不透明的UI元素,使用不透明Shader:Unity UI有一个
UI/Default的变体是给不透明元素用的,它关闭了混合,性能更好。确保你的纯色背景、不透明图标等使用这个Shader。 - 谨慎使用
Canvas组件的Additional Shader Channels:如果UI Shader需要法线、切线等信息,需要在这里开启,但会增加带宽开销。 - 对于复杂的UI淡入淡出,考虑使用Canvas Group的Alpha,而不是直接修改每个子元素材质的Alpha。前者通常更高效,因为它是在合批后的网格层面进行整体Alpha混合。
6. 性能考量与平台差异
混合不是一个“免费”的操作。开启混合,尤其是关闭深度写入后,GPU需要做更多的工作,并且会破坏一些重要的优化。
- Early-Z/HSR(早期深度测试/隐藏面消除)失效:现代GPU有一个在片元着色器执行前进行的早期深度测试阶段,可以提前丢弃那些肯定会被遮挡的片元,节省大量计算。但是,这个优化通常要求开启深度写入。当我们为透明物体关闭深度写入(
ZWrite Off)时,Early-Z优化就失效了。这意味着所有透明物体的片元,无论是否最终可见,都会进入昂贵的片元着色器进行计算。这是透明渲染耗性能的主要原因之一。 - 带宽压力:混合需要读取当前帧缓冲中的目标颜色(
DstColor),然后进行计算,再写回。这个“读取-修改-写入”的过程增加了显存带宽的消耗。在移动平台的Tile-Based架构上(如PowerVR, Mali, Adreno),混合操作发生在芯片上的快速Tile Memory中,相对高效。但在桌面GPU上,频繁的混合会对带宽造成压力。 - 顺序依赖:正确的Alpha混合依赖于从后往前的渲染顺序。这要求CPU端进行排序,增加了CPU开销。对于大量、动态的透明物体(如森林中的树叶),排序本身就是一个性能负担,而且排序结果可能不完美,导致渲染瑕疵。
平台差异小提示:
- 一些非常老的移动GPU或低端设备,对某些特殊的
BlendOp(如Min/Max)支持可能不好,需要测试。 - 在编写跨平台Shader时,使用Unity内置的宏和因子名是最安全的选择,Unity会帮你处理不同图形API(OpenGL, Direct3D, Metal, Vulkan)之间的细微差异。
7. 进阶:混合与渲染目标(Render Target)
我们之前讨论的混合,默认都是针对主帧缓冲(即显示在屏幕上的那个缓冲区)进行的。但在现代渲染中,我们经常使用多渲染目标(Multiple Render Targets, MRT)或离屏渲染(Off-screen Render Texture)到自定义的缓冲区中。混合同样适用于这些自定义的渲染目标。
在Shader中,你可以使用Blend指令的另一种形式来为不同的渲染目标索引设置不同的混合状态:
// 语法:Blend <RenderTargetIndex> <SrcFactor> <DstFactor> Blend 0 SrcAlpha OneMinusSrcAlpha // 对第一个渲染目标(索引0)使用Alpha混合 Blend 1 One One // 对第二个渲染目标(索引1)使用加法混合这在延迟渲染(Deferred Rendering)的自定义光照阶段、或者需要同时输出颜色和自发光(Emissive)到不同缓冲区的后处理效果中非常有用。在Shader Graph中,你可以通过创建多个Fragment输出端口,并分别为它们指定混合模式来实现类似功能(需要相应的渲染管线支持)。
一个实际案例:屏幕空间反射(SSR)的积累在实现屏幕空间反射时,我们可能需要将多次射线步进(Ray Marching)的结果混合起来。第一次步进的结果写入一个渲染纹理(Render Texture),第二次步进时,需要将新结果与纹理中已有的结果进行混合(比如使用Blend SrcAlpha OneMinusSrcAlpha来根据射线置信度进行混合)。这时,你就是在两个纹理之间进行混合操作,而不是直接与主帧缓冲混合。
理解到这一层,你对混合的掌控就不再局限于简单的材质透明了,而是可以将其作为一种通用的图像合成工具,用在渲染管线的各个阶段。
8. 调试与常见问题排查
当你发现混合效果不对时,可以按照以下思路排查:
- 检查渲染队列(Queue):这是新手最常犯的错误。透明物体必须使用
Transparent队列。在Inspector面板的材质球上,你可以看到Rendering Mode选项,选择Transparent会自动设置正确的Queue和混合模式。如果手动写Shader,务必在Tags中设置。 - 检查深度写入(ZWrite):对于真正的半透明,确保是
ZWrite Off。对于需要写入深度的特殊效果(比如作为遮罩的透明物体),再考虑开启。 - 检查混合指令是否正确:确认
Blend后面的因子是否是你想要的。用Blend One One想做半透明,效果肯定是错的。 - 检查Alpha源:你的片元着色器最终输出的颜色
fixed4(col, alpha)中的alpha值是否正确?是不是一直是1?纹理的Alpha通道是否包含有效数据?记得在Properties中声明[HDR] _Color或_TintColor时,其Alpha分量也会参与混合。 - 检查渲染顺序:如果多个透明物体交错,顺序不对会导致颜色异常。尝试在Shader的
Tags中调整"Queue"="Transparent+xxx"的数值,或者通过脚本动态调整物体的渲染顺序。 - 使用Frame Debugger:Unity的Frame Debugger(窗口 -> Analysis -> Frame Debugger)是神器。你可以一帧一帧地查看Draw Call,选中任何一个Draw Call,在右侧详情面板中可以看到该次绘制使用的Shader状态,包括
Blend和ZWrite的设置,以及当前渲染目标的内容。这是验证你的混合设置是否生效的最直接方法。 - 检查是否被后处理覆盖:如果你的透明物体在Scene视图显示正常,但在Game视图不正常,检查一下是否启用了全屏后处理效果(如Bloom, Color Grading)。某些后处理效果可能会改变透明物体的混合结果,或者后处理本身需要正确的透明通道输入。
混合模式是Shader编写中看似基础,实则充满细节的一环。它连接着美术效果与硬件渲染,理解其背后的数学原理和性能影响,能让你在实现炫酷效果的同时,更好地掌控项目的渲染效率。下次当你需要让某个东西“透”过去或者“亮”起来的时候,希望你能自信地写出正确的Blend指令,而不是盲目地复制粘贴。