GameTech/URP 渲染实战

案例 26.5径向模糊入门

目标:冲刺、爆炸冲击时的“速度感”:画面沿着指向某个中心的方向被拉出拖影,中心附近保持清晰。

原理:对每个像素,沿“像素 → 中心”方向在一段距离内均匀采样若干次求平均。拖影长度正比于像素到中心的距离,所以边缘模糊、中心清晰,这正是镜头快速推进时的观感。为了避免采样次数少时出现分层的“重影”,每个像素的起始位置加一个 0~1 的抖动,用的是 Interleaved Gradient Noise(Jimenez 2014),把规则的分层变成细密、均匀的噪点:

Assets/Cookbook/Ch26/RadialBlur.shader(节选:frag)
half4 Frag(Varyings input) : SV_Target
{
    UNITY_SETUP_STEREO_EYE_INDEX_POST_VERTEX(input);
    float2 uv = input.texcoord;
    float2 center = _RadialParams.xy;
    float2 dir = uv - center;                       // 指向外侧,长度 = 到中心的距离

    // 中心一圈保持清晰:按宽高比修正成正圆后再算距离
    float aspect = _ScreenParams.x / _ScreenParams.y;
    float dist = length(dir * float2(aspect, 1.0));
    float amount = _RadialParams.z * smoothstep(_ClearRadius, _ClearRadius + 0.25, dist);

    int count = (int)_RadialParams.w;
    // 每个像素的起点加一点抖动,把“分层的重影”变成细噪点
    float jitter = InterleavedGradientNoise(uv * _ScreenParams.xy);
    half3 sum = 0;
    [loop] for (int i = 0; i < count; i++)
    {
        float t = (i + jitter) / count;             // 0..1
        float2 s = uv - dir * amount * 0.3 * t;     // 向中心方向收缩
        sum += SAMPLE_TEXTURE2D_X(_BlitTexture, sampler_LinearClamp, s).rgb;
    }
    return half4(sum / count, 1.0);
}
vec4 Frag()
{
    vec2 uv = texcoord;
    vec2 center = _RadialParams.xy;
    vec2 dir = uv - center;                         // 指向外侧,长度 = 到中心的距离

    // 中心一圈保持清晰:按宽高比修正成正圆后再算距离
    float aspect = _ScreenParams.x / _ScreenParams.y;
    float dist = length(dir * vec2(aspect, 1.0));
    float amount = _RadialParams.z * smoothstep(_ClearRadius, _ClearRadius + 0.25, dist);

    int count = int(_RadialParams.w);
    // 每个像素的起点加一点抖动,把“分层的重影”变成细噪点
    float jitter = InterleavedGradientNoise(uv * _ScreenParams.xy);
    vec3 sum = vec3(0.0);
    for (int i = 0; i < count; i++)
    {
        float t = (float(i) + jitter) / float(count);   // 0..1
        vec2 s = uv - dir * amount * 0.3 * t;           // 向中心方向收缩
        sum += texture(_BlitTexture, s).rgb;
    }
    return vec4(sum / float(count), 1.0);
}

C# 只有参数传递(使用 SimplePostPass):

Assets/Cookbook/Ch26/RadialBlur.cs(节选:pass)
class RadialBlurPass : SimplePostPass<RadialBlurVolume>
{
    static readonly int s_Params = Shader.PropertyToID("_RadialParams");   // xy 中心, z 强度, w 采样数
    static readonly int s_ClearRadius = Shader.PropertyToID("_ClearRadius");
    public RadialBlurPass(Material m) : base(m, "Ch26 Radial Blur") { }

    protected override void Apply(RadialBlurVolume v, ref RenderingData renderingData)
    {
        var c = v.center.value;
        material.SetVector(s_Params, new Vector4(c.x, c.y, v.strength.value, v.samples.value));
        material.SetFloat(s_ClearRadius, v.clearRadius.value);
    }
}
图 26-12左:原图;右:strength = 0.6、16 次采样、中心 (0.5, 0.55)、clearRadius = 0.1

运行时把 strength 交给 Volume 权重动画(参见案例 26.10 的写法),就能做出冲刺开始时猛地一拉、随后迅速恢复的效果。中心点也可以每帧设为某个世界坐标投影到屏幕的位置(Camera.WorldToViewportPoint),做成“朝着爆炸点”的冲击模糊。