URP 渲染实战 / 第 26 章 自定义后处理 / 案例 26.10
案例 26.10受击红屏进阶
目标:角色受到伤害时,屏幕边缘泛起带有不规则“血丝”的红色,中间区域轻微压暗去色,随后在半秒左右淡出;强度可以随伤害大小变化。
原理:效果本身是一个边缘遮罩:椭圆形的距离场,用 fBm 噪声扰动边界产生不规则的“血丝”,强度越大红色往中心侵入越深。混合时不用简单的 lerp 到红色——那会把画面边缘变成一片纯色——而是“乘 + 加”:原图乘以红色保留明暗细节,再加上一点红色保证暗部也被染红:
Assets/Cookbook/Ch26/HitFlash.shader(节选:frag)
half4 Frag(Varyings input) : SV_Target { UNITY_SETUP_STEREO_EYE_INDEX_POST_VERTEX(input); float2 uv = input.texcoord; half4 color = SAMPLE_TEXTURE2D_X(_BlitTexture, sampler_LinearClamp, uv); float k = _HitParams.x; // 1) 椭圆形的边缘遮罩:强度越大,红色往中心侵入越深 float2 d = (uv - 0.5) * 2.0; float r = length(d * float2(1.0, 0.85)); // 2) 用 fBm 噪声扰动遮罩边界,形成不规则的“血丝”;相位让它在运行时蠕动 float t = _HitParams.z + _Time.y * 0.6; float n = Fbm(uv * _HitParams.y + float2(t, -t * 0.7)); float inner = lerp(1.25, 0.45, k); half edge = smoothstep(inner, inner + 0.55, r + (n - 0.5) * 0.45); // 3) 中心区域轻微去色、压暗,模拟“眼前一黑” half luma = dot(color.rgb, half3(0.2126, 0.7152, 0.0722)); half3 dimmed = lerp(color.rgb, luma.xxx * half3(1.0, 0.85, 0.85), 0.35 * k) * (1.0 - 0.2 * k); // 4) 边缘:用乘 + 加的组合,暗部也能被染红,亮部不会变成纯色块 half3 blood = dimmed * _HitColor.rgb * 0.6 + _HitColor.rgb * 0.35; color.rgb = lerp(dimmed, blood, edge * saturate(k * 1.5)); return color; }
vec4 Frag() { vec2 uv = texcoord; vec4 color = texture(_BlitTexture, uv); float k = _HitParams.x; // 1) 椭圆形的边缘遮罩:强度越大,红色往中心侵入越深 vec2 d = (uv - 0.5) * 2.0; float r = length(d * vec2(1.0, 0.85)); // 2) 用 fBm 噪声扰动遮罩边界,形成不规则的“血丝”;相位让它在运行时蠕动 float t = _HitParams.z + _Time.y * 0.6; float n = Fbm(uv * _HitParams.y + vec2(t, -t * 0.7)); float inner = mix(1.25, 0.45, k); float edge = smoothstep(inner, inner + 0.55, r + (n - 0.5) * 0.45); // 3) 中心区域轻微去色、压暗,模拟“眼前一黑” float luma = dot(color.rgb, vec3(0.2126, 0.7152, 0.0722)); vec3 dimmed = mix(color.rgb, vec3(luma) * vec3(1.0, 0.85, 0.85), 0.35 * k) * (1.0 - 0.2 * k); // 4) 边缘:用乘 + 加的组合,暗部也能被染红,亮部不会变成纯色块 vec3 blood = dimmed * _HitColor.rgb * 0.6 + _HitColor.rgb * 0.35; color.rgb = mix(dimmed, blood, edge * saturate(k * 1.5)); return color; }

怎么驱动它:这个效果天然是“瞬时”的,最干净的做法是让 Volume 权重来做动画,而不是每帧去改 Profile 里的参数:
- 场景里放一个全局 Volume,Profile 中只添加 Hit Flash 覆盖项,intensity = 1。
- 给它挂上下面的
HitFlashTrigger,受击时调用Hit(伤害比例)。
Assets/Cookbook/Ch26/HitFlashTrigger.cs(节选:trigger)
/// 挂在一个全局 Volume(Is Global 勾选,Profile 里只放 HitFlashVolume 且 intensity=1)上。 /// 受击时调用 Hit():Volume 权重瞬间拉到 1 再按曲线衰减到 0。权重为 0 时栈里的 intensity 回到默认值 0, /// Feature 的 IsActive() 为假,整个 Pass 不入队,平时没有任何开销。 [RequireComponent(typeof(Volume))] public class HitFlashTrigger : MonoBehaviour { public float duration = 0.6f; public AnimationCurve falloff = AnimationCurve.EaseInOut(0, 1, 1, 0); Volume m_Volume; float m_Elapsed = float.MaxValue; void Awake() { m_Volume = GetComponent<Volume>(); m_Volume.weight = 0f; } public void Hit(float strength = 1f) { m_Elapsed = 0f; m_Volume.weight = strength; } void Update() { if (m_Elapsed > duration) { m_Volume.weight = 0f; return; } m_Elapsed += Time.deltaTime; m_Volume.weight = falloff.Evaluate(Mathf.Clamp01(m_Elapsed / duration)); } }
这样做的好处是:Profile 资产不会在运行时被改脏(直接改 sharedProfile 里的参数,退出 Play 模式后改动会留在资产里);多个来源的受击可以用多个 Volume 叠加;权重回到 0 后,栈里的 intensity 回到默认值 0,IsActive() 为假,Pass 不入队,平时没有任何开销。这个“用 Volume 权重做后处理动画”的模式同样适用于径向模糊、低血量时的持续暗角、进入水下时的色调等。
注意:记得 26.1 节的提醒:如果相机的 Volume Update Mode 是 Via Scripting,权重变化不会自动反映到栈里。另外,Volume.weight 是对整个 Profile 生效的,所以给这类“动画用”的 Volume 单独一个 Profile,不要和常驻的调色、Bloom 放在一起。