URP 渲染实战 / 第 21 章 UV 动画与粒子 / 案例 21.5
案例 21.5通用粒子 Shader:顶点色与混合模式入门
目标:写一个可以替代 Particles/Unlit 大部分用途的粒子 Shader,理解几种混合模式的差别。
Assets/Cookbook/Ch21/Particle.shader(节选:streams)
// 与粒子系统 Renderer › Custom Vertex Streams 的顺序一一对应: // Position(POSITION) · Color(COLOR) · UV(TEXCOORD0.xy) · UV2(TEXCOORD0.zw) · AnimBlend(TEXCOORD1.x) struct Attributes { float4 positionOS : POSITION; half4 color : COLOR; #if defined(_FLIPBOOKBLENDING_ON) float4 texcoords : TEXCOORD0; // xy = 当前帧 UV,zw = 下一帧 UV float animBlend : TEXCOORD1; // 两帧之间的进度 0..1 #else float2 texcoords : TEXCOORD0; #endif UNITY_VERTEX_INPUT_INSTANCE_ID };
Assets/Cookbook/Ch21/Particle.shader(节选:frag)
half4 frag(Varyings i) : SV_Target { half4 tex = SAMPLE_TEXTURE2D(_BaseMap, sampler_BaseMap, i.uv.xy); #if defined(_FLIPBOOKBLENDING_ON) half4 tex2 = SAMPLE_TEXTURE2D(_BaseMap, sampler_BaseMap, i.uv.zw); tex = lerp(tex, tex2, i.extra.x); #endif // 顶点色 = 粒子系统的 Start Color × Color over Lifetime:整个粒子系统的颜色变化都从这里进来 half4 c = tex * i.color * _BaseColor; #if defined(_RAMP_ON) // 渐变映射:用“贴图浓度 × 粒子剩余寿命(顶点色 alpha)”去查火焰渐变,越老越暗红 half k = saturate(tex.a * i.color.a * _RampScale); c.rgb = SAMPLE_TEXTURE2D(_RampTex, sampler_RampTex, float2(k, 0.5)).rgb * _BaseColor.rgb; c.a = saturate(k); #endif float thisZ = i.screenData.z; #if defined(_SOFTPARTICLES_ON) // 软粒子:比较场景深度与粒子自身深度,越接近(快要插进物体)越透明 float2 suv = i.screenData.xy / i.screenData.w; float sceneZ = LinearEyeDepth(SampleSceneDepth(suv), _ZBufferParams); c.a *= saturate((sceneZ - thisZ) / _SoftDistance); #endif #if defined(_CAMERAFADE_ON) // 近相机淡出:粒子贴脸时会糊满屏幕、产生巨大 Overdraw,提前淡掉 c.a *= saturate((thisZ - _CameraFadeNear) / max(_CameraFadeFar - _CameraFadeNear, 1e-3)); #endif c.rgb *= lerp(1.0, c.a, _Premultiply); // 预乘:Blend One OneMinusSrcAlpha 时需要 // 正片叠底(Blend DstColor Zero)没有 alpha 参与:把 alpha 折算进颜色,透明处趋向白色(=不改变背景) c.rgb = lerp(c.rgb, lerp(1.0, c.rgb, c.a), _Modulate); // 雾:Alpha 混合直接 MixFog;叠加模式应往黑色雾化 MixFogColor(c.rgb, 0, fog),否则雾里越叠越亮 c.rgb = MixFog(c.rgb, i.extra.y); return c; }
vec4 frag() { vec4 tex = texture(_BaseMap, uv.xy); #if defined(_FLIPBOOKBLENDING_ON) vec4 tex2 = texture(_BaseMap, uv.zw); tex = mix(tex, tex2, extra.x); #endif // 顶点色 = 粒子系统的 Start Color × Color over Lifetime:整个粒子系统的颜色变化都从这里进来 vec4 c = tex * vColor * _BaseColor; #if defined(_RAMP_ON) // 渐变映射:用“贴图浓度 × 粒子剩余寿命(顶点色 alpha)”去查火焰渐变,越老越暗红 float k = saturate(tex.a * vColor.a * _RampScale); c.rgb = texture(_RampTex, vec2(k, 0.5)).rgb * _BaseColor.rgb; c.a = saturate(k); #endif float thisZ = screenData.z; #if defined(_SOFTPARTICLES_ON) // 软粒子:比较场景深度与粒子自身深度,越接近(快要插进物体)越透明 vec2 suv = screenData.xy / screenData.w; float sceneZ = LinearEyeDepth(SampleSceneDepth(suv), _ZBufferParams); c.a *= saturate((sceneZ - thisZ) / _SoftDistance); #endif #if defined(_CAMERAFADE_ON) // 近相机淡出:粒子贴脸时会糊满屏幕、产生巨大 Overdraw,提前淡掉 c.a *= saturate((thisZ - _CameraFadeNear) / max(_CameraFadeFar - _CameraFadeNear, 1e-3)); #endif c.rgb *= mix(1.0, c.a, _Premultiply); // 预乘:Blend One OneMinusSrcAlpha 时需要 // 正片叠底(Blend DstColor Zero)没有 alpha 参与:把 alpha 折算进颜色,透明处趋向白色(=不改变背景) c.rgb = mix(c.rgb, mix(vec3(1.0), c.rgb, c.a), _Modulate); // 雾:Alpha 混合直接 MixFog;叠加模式应往黑色雾化 MixFogColor(c.rgb, 0, fog),否则雾里越叠越亮 c.rgb = MixFog(c.rgb, extra.y); return c; }
混合模式用 Blend [_SrcBlend] [_DstBlend] 做成材质参数,同一个 Shader 不同材质就能是不同的模式。粒子常用的几种:
| 模式 | Blend 设置 | 结果 | 典型用途 |
|---|---|---|---|
| Alpha | SrcAlpha OneMinusSrcAlpha | 按 alpha 覆盖背景,能遮挡 | 烟、灰尘、碎片 |
| Additive | SrcAlpha One(或 One One) | 只加亮,不遮挡,与顺序无关 | 火花、魔法光效、光晕 |
| Premultiply | One OneMinusSrcAlpha | RGB 已预乘 alpha;alpha 小而 RGB 大时表现为发光 | 火焰核心(一张图里既有遮挡又有发光) |
| Multiply | DstColor Zero | 只会变暗 | 焦痕、阴影斑、暗色烟 |
正片叠底没有 alpha 参与运算,透明的地方必须输出白色才不改变背景;代码里的 _Modulate 就是把 alpha 折算进颜色:lerp(1, rgb, a),与 URP 的 AlphaModulate 函数做法相同。
由于批处理模式下粒子系统的拍摄需要额外处理(见案例 21.7),这一组对比图用一组朝向相机的 Quad 手工摆出“一团粒子”,每个 Quad 的网格写入不同的顶点色,模拟粒子的 Start Color——Shader 看到的数据与真实粒子并无二致:
Assets/Cookbook/Ch21/Editor/Ch21Shots.cs(节选:quadParticles)
/// 用一组朝向相机的 Quad 摆出“一团粒子”,每个 Quad 带不同的顶点色(模拟粒子的 Start Color) static void FakeParticles(Shot s, Material m, Vector3 center, int count, Vector3 spread, float size, Color c0, Color c1, int seed) { var rnd = new System.Random(seed); for (int i = 0; i < count; i++) { var p = center + Vector3.Scale(new Vector3((float)rnd.NextDouble() - 0.5f, (float)rnd.NextDouble() - 0.5f, (float)rnd.NextDouble() - 0.5f), spread); var q = s.Quad(m, p, size * Mathf.Lerp(0.7f, 1.2f, (float)rnd.NextDouble())); q.transform.rotation = Quaternion.LookRotation(p - s.Cam.transform.position) * Quaternion.Euler(0, 0, (float)rnd.NextDouble() * 360); var mesh = Object.Instantiate(q.GetComponent<MeshFilter>().sharedMesh); var col = Color.Lerp(c0, c1, (float)rnd.NextDouble()); mesh.colors = new[] { col, col, col, col }; q.GetComponent<MeshFilter>().sharedMesh = mesh; s.Temp.Add(mesh); } }

Additive 的两种写法
SrcAlpha One 用 alpha 控制亮度,贴图带 alpha 通道时更直观;One One 直接把 RGB 加上去,要求贴图本身在透明处是黑色。粒子贴图如果是在黑底上画的(很多火焰素材是这样),用后者即可,还省一次乘法。