ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

Unity Shader混合模式详解:从Alpha混合到实战应用

Unity Shader混合模式详解:从Alpha混合到实战应用 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 { QueueTransparent RenderTypeTransparent } 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。如果你的透明物体没有被正确排序会出现奇怪的遮挡问题。对于复杂的透明物体有时需要使用QueueTransparent100来手动调整渲染顺序。避坑点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队列并且关闭深度写入。但加法混合有一个特性它的结果与渲染顺序无关因为满足交换律AB和BA结果一样。所以对于纯加法混合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 UIuGUI的默认材质使用的就是标准的Blend SrcAlpha OneMinusSrcAlpha。但在UI中混合问题常常以另一种形式出现Canvas的渲染顺序和Overdraw。UI元素在同一个Canvas下其渲染顺序由Hierarchy中的顺序决定后绘制的在上层。当你有大量半透明UI重叠时比如多个半透明面板、窗口Overdraw会急剧上升。更棘手的是UI系统默认的合批Batching规则可能会因为层级顺序而被打断导致Draw Call增加。优化建议尽可能减少UI的层级和重叠这是根本。对不透明的UI元素使用不透明ShaderUnity 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会帮你处理不同图形APIOpenGL, 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中调整QueueTransparentxxx的数值或者通过脚本动态调整物体的渲染顺序。使用Frame DebuggerUnity的Frame Debugger窗口 - Analysis - Frame Debugger是神器。你可以一帧一帧地查看Draw Call选中任何一个Draw Call在右侧详情面板中可以看到该次绘制使用的Shader状态包括Blend和ZWrite的设置以及当前渲染目标的内容。这是验证你的混合设置是否生效的最直接方法。检查是否被后处理覆盖如果你的透明物体在Scene视图显示正常但在Game视图不正常检查一下是否启用了全屏后处理效果如Bloom, Color Grading。某些后处理效果可能会改变透明物体的混合结果或者后处理本身需要正确的透明通道输入。混合模式是Shader编写中看似基础实则充满细节的一环。它连接着美术效果与硬件渲染理解其背后的数学原理和性能影响能让你在实现炫酷效果的同时更好地掌控项目的渲染效率。下次当你需要让某个东西“透”过去或者“亮”起来的时候希望你能自信地写出正确的Blend指令而不是盲目地复制粘贴。
返回列表