URP 渲染实战 / 第 16 章 透明、混合与模板测试 / 案例 16.1
案例 16.1混合模式大比拼入门
目标:用一个 Shader 切出六种常见混合模式,在同一个背景(上半黑→白渐变、下半色相条)上叠三张红绿蓝圆盘,直观对比。
完整代码:
Assets/Cookbook/Ch16/BlendModes.shader
// 混合模式实验台:源/目标因子和混合运算都做成材质属性,同一个 Shader 切出所有常见模式。 // _Output 决定片元输出的形式:直通(rgb, a)/ 预乘(贴图已预乘)/ 乘法就绪(lerp(1, rgb, a))。 Shader "Cookbook/Ch16/BlendModes" { Properties { [MainTexture] _BaseMap ("Base Map (alpha = shape)", 2D) = "white" {} [MainColor] _BaseColor ("Color", Color) = (1, 0.3, 0.2, 0.75) [Enum(UnityEngine.Rendering.BlendMode)] _SrcBlend ("Src Blend", Float) = 5 // SrcAlpha [Enum(UnityEngine.Rendering.BlendMode)] _DstBlend ("Dst Blend", Float) = 10 // OneMinusSrcAlpha [Enum(UnityEngine.Rendering.BlendOp)] _BlendOp ("Blend Op", Float) = 0 // Add [KeywordEnum(Straight, Premultiply, MultiplyReady)] _Output ("Fragment Output", Float) = 0 } SubShader { Tags { "RenderType"="Transparent" "RenderPipeline"="UniversalPipeline" "Queue"="Transparent" "IgnoreProjector"="True" } Pass { Name "Blend" Tags { "LightMode"="UniversalForward" } BlendOp [_BlendOp] Blend [_SrcBlend] [_DstBlend] ZWrite Off Cull Off HLSLPROGRAM #pragma vertex vert #pragma fragment frag #pragma shader_feature_local_fragment _OUTPUT_STRAIGHT _OUTPUT_PREMULTIPLY _OUTPUT_MULTIPLYREADY #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl" CBUFFER_START(UnityPerMaterial) float4 _BaseMap_ST; half4 _BaseColor; float _SrcBlend, _DstBlend, _BlendOp; CBUFFER_END TEXTURE2D(_BaseMap); SAMPLER(sampler_BaseMap); struct Attributes { float4 positionOS : POSITION; float2 uv : TEXCOORD0; }; struct Varyings { float4 positionCS : SV_POSITION; float2 uv : TEXCOORD0; }; Varyings vert(Attributes v) { Varyings o; o.positionCS = TransformObjectToHClip(v.positionOS.xyz); o.uv = TRANSFORM_TEX(v.uv, _BaseMap); return o; } half4 frag(Varyings i) : SV_Target { half4 tex = SAMPLE_TEXTURE2D(_BaseMap, sampler_BaseMap, i.uv); half3 rgb = tex.rgb * _BaseColor.rgb; half a = tex.a * _BaseColor.a; #if defined(_OUTPUT_PREMULTIPLY) // 预乘:贴图的 rgb 已经乘过 alpha(导出时完成),这里只再乘上材质的不透明度。 // 于是 rgb 可以“大于” alpha——alpha = 0 而 rgb > 0 的像素就是纯叠加的辉光 return half4(tex.rgb * _BaseColor.rgb * _BaseColor.a, a); #elif defined(_OUTPUT_MULTIPLYREADY) return half4(lerp(1.0h, rgb, a), a); // 乘法类:透明处输出白色(乘 1 不变) #else return half4(rgb, a); // 直通 #endif } ENDHLSL } } }
// 混合模式实验台:源/目标因子和混合运算都做成材质属性,同一个 Shader 切出所有常见模式。(GLSL 版) // _Output 决定片元输出的形式:直通(rgb, a)/ 预乘(贴图已预乘)/ 乘法就绪(lerp(1, rgb, a))。 Shader "Cookbook/Ch16/BlendModes" { SubShader { Tags { "RenderType"="Transparent" "RenderPipeline"="UniversalPipeline" "Queue"="Transparent" "IgnoreProjector"="True" } Pass { Name "Blend" Tags { "LightMode"="UniversalForward" } BlendOp [_BlendOp] Blend [_SrcBlend] [_DstBlend] ZWrite Off Cull Off GLSLPROGRAM #pragma shader_feature_local_fragment _OUTPUT_STRAIGHT _OUTPUT_PREMULTIPLY _OUTPUT_MULTIPLYREADY uniform vec4 _BaseMap_ST; uniform vec4 _BaseColor; uniform sampler2D _BaseMap; VARYING vec2 uv; #ifdef VERTEX void main() { gl_Position = TransformObjectToHClip(positionOS.xyz); uv = TransformTex(texcoord0.xy, _BaseMap_ST); } #endif #ifdef FRAGMENT vec4 frag() { vec4 tex = texture(_BaseMap, uv); vec3 rgb = tex.rgb * _BaseColor.rgb; float a = tex.a * _BaseColor.a; #if defined(_OUTPUT_PREMULTIPLY) // 预乘:贴图的 rgb 已经乘过 alpha(导出时完成),这里只再乘上材质的不透明度。 // 于是 rgb 可以“大于” alpha——alpha = 0 而 rgb > 0 的像素就是纯叠加的辉光 return vec4(tex.rgb * _BaseColor.rgb * _BaseColor.a, a); #elif defined(_OUTPUT_MULTIPLYREADY) return vec4(mix(vec3(1.0), rgb, a), a); // 乘法类:透明处输出白色(乘 1 不变) #else return vec4(rgb, a); // 直通 #endif } void main() { fragColor = frag(); } #endif ENDGLSL } } }
渲染状态全部引用材质属性:
Assets/Cookbook/Ch16/BlendModes.shader(节选:state)
片元输出有三种形式,对应不同混合模式对“源颜色”的约定:
Assets/Cookbook/Ch16/BlendModes.shader(节选:frag)
half4 frag(Varyings i) : SV_Target { half4 tex = SAMPLE_TEXTURE2D(_BaseMap, sampler_BaseMap, i.uv); half3 rgb = tex.rgb * _BaseColor.rgb; half a = tex.a * _BaseColor.a; #if defined(_OUTPUT_PREMULTIPLY) // 预乘:贴图的 rgb 已经乘过 alpha(导出时完成),这里只再乘上材质的不透明度。 // 于是 rgb 可以“大于” alpha——alpha = 0 而 rgb > 0 的像素就是纯叠加的辉光 return half4(tex.rgb * _BaseColor.rgb * _BaseColor.a, a); #elif defined(_OUTPUT_MULTIPLYREADY) return half4(lerp(1.0h, rgb, a), a); // 乘法类:透明处输出白色(乘 1 不变) #else return half4(rgb, a); // 直通 #endif }
vec4 frag() { vec4 tex = texture(_BaseMap, uv); vec3 rgb = tex.rgb * _BaseColor.rgb; float a = tex.a * _BaseColor.a; #if defined(_OUTPUT_PREMULTIPLY) // 预乘:贴图的 rgb 已经乘过 alpha(导出时完成),这里只再乘上材质的不透明度。 // 于是 rgb 可以“大于” alpha——alpha = 0 而 rgb > 0 的像素就是纯叠加的辉光 return vec4(tex.rgb * _BaseColor.rgb * _BaseColor.a, a); #elif defined(_OUTPUT_MULTIPLYREADY) return vec4(mix(vec3(1.0), rgb, a), a); // 乘法类:透明处输出白色(乘 1 不变) #else return vec4(rgb, a); // 直通 #endif }
| 模式 | Blend 设置 | 结果 | 典型用途 |
|---|---|---|---|
| Alpha(传统透明) | SrcAlpha OneMinusSrcAlpha | S·a + D·(1−a) | 玻璃、半透明 UI、淡出 |
| 预乘 Alpha | One OneMinusSrcAlpha | S′ + D·(1−a),S′ = S·a | 同时表达“遮挡”和“发光”、正确的过滤与合成 |
| Additive(叠加) | SrcAlpha One 或 One One | S·a + D | 火焰、光束、能量特效 |
| Soft Additive(滤色) | OneMinusDstColor One | S + D − S·D = 1 − (1−S)(1−D) | 柔和的光晕,不容易过曝 |
| Multiply(正片叠底) | DstColor Zero | S·D | 阴影贴片、染色、污渍 |
| 2× Multiply | DstColor SrcColor | 2·S·D | 既能变亮也能变暗(S > 0.5 变亮)的细节叠加 |
| Max(变亮) | BlendOp Max | max(S, D) | 多层光斑取最亮,不累加 |


预乘 Alpha 为什么重要
预乘(Premultiplied Alpha)把“颜色乘以覆盖率”这一步提前到贴图导出时完成,Shader 输出的 rgb 已经是 S·a。这带来三个好处:
- 遮挡和发光可以共存:rgb 与 alpha 是独立的。alpha 决定“挡住多少背景”,rgb 决定“加上多少光”。alpha = 0、rgb > 0 的像素就是纯发光——图 16-2 中图的辉光就是这样做出来的,一个材质同时完成了 Alpha 混合和 Additive 两种工作,特效可以少一个 Draw Call。
- 过滤正确:直通 Alpha 的贴图在透明区域的 rgb 往往是无意义的值(常见是黑色),双线性过滤、mip 生成时会把这些黑色混进边缘,出现一圈暗边。预乘后透明区域的 rgb 一定是 0,过滤结果天然正确。
- 合成满足结合律:多层预乘图像可以先两两合成再整体合成,结果不变——这对把 UI 或特效先渲染到 RT 再合成的流程非常重要。
常见坑① URP 默认使用 HDR 颜色缓冲,Additive 可以让颜色超过 1,配合 Bloom 很漂亮;但 Soft Additive 的因子
OneMinusDstColor 在目标值大于 1 时变成负数,会把颜色“减”掉,HDR 下请慎用。② Multiply 类模式要求透明区域输出白色(乘 1 不变),所以片元输出要用 lerp(1, rgb, a),直接输出 rgb 会把透明区域染黑。③ 在线性颜色空间工程里,混合发生在线性值上(sRGB 目标会在读写时自动转换),半透明边缘的观感和 Photoshop 默认的 Gamma 空间混合不同,美术对比参考图时需要知道这一点。④ Min/Max 运算会忽略混合因子,直接逐分量取最小/最大值。对照:URP Lit 的 Blending Mode 是怎么设置的
URP Lit 材质的 Surface Type 选 Transparent 后,Blending Mode 有四个选项。它们最终都落到材质的 _SrcBlend/_DstBlend/_SrcBlendAlpha/_DstBlendAlpha 四个属性上,由编辑器脚本 BaseShaderGUI.cs 设置(摘要如下,以 URP 14 源码为准):
| Blending Mode | RGB 因子 | Alpha 因子 | Shader 里的配合 |
|---|---|---|---|
| Alpha | SrcAlpha, OneMinusSrcAlpha | One, OneMinusSrcAlpha | — |
| Premultiply | One, OneMinusSrcAlpha | One, OneMinusSrcAlpha | 关键字 _ALPHAPREMULTIPLY_ON |
| Additive | SrcAlpha, One | One, One | — |
| Multiply | DstColor, Zero | Zero, One(保留目标 Alpha) | 关键字 _ALPHAMODULATE_ON,Shader 输出 lerp(1, rgb, a) |
注意 Alpha 模式下 Alpha 通道用的是 One, OneMinusSrcAlpha 而不是和 RGB 一样的因子——正是上面说的“覆盖率累积”。另外还有一个 Preserve Specular Lighting 选项(Alpha 与 Additive 模式可用):勾选后 RGB 的源因子被改成 One,由 Shader 自己只把漫反射部分乘上 alpha,高光部分不乘。物理上这很合理——一块几乎全透明的玻璃,表面反射的高光并不会因为玻璃透明而变弱。用前面的说法,这就是“预乘 Alpha,但只预乘漫反射”。如果你自己写的透明 Shader 里玻璃的高光总是灰蒙蒙的,原因多半就在这里。