GameTech/URP 渲染实战

案例 26.3Kawase 与 Dual Kawase 模糊进阶

目标:实现移动端常用的两种廉价大半径模糊,并和高斯对比。

Kawase 模糊:每次迭代只在对角线 4 个点各采样一次,偏移随迭代递增(第 i 次为 i + 0.5 纹素)。每次采样落在 4 个纹素中间,双线性过滤让每个采样本身就是一次 2×2 平均,所以 4 次采样覆盖了 16 个纹素;迭代几次之后,结果非常接近高斯。它的半径随迭代次数大约线性增长,每次迭代固定 4 次采样。

Dual Kawase(Marius Bjørge 在 SIGGRAPH 2015 移动图形课程中提出):把迭代放到降采样金字塔上——下行时每级分辨率减半,用 5 次采样的核;上行时每级分辨率加倍,用 8 次采样的核。因为越往下分辨率越低,每级的像素数呈 1/4 递减,总开销几乎与级数无关,而模糊半径随级数指数增长。

下采样:5 次采样 4 (4·中心 + 4 个对角) / 8 上采样:8 次采样 (菱形 4×1 + 对角 4×2) / 12 金字塔 全分辨率½
图 26-7Dual Kawase 的两个采样核(橙色权重更大,网格为源纹理的纹素)。下采样时输出像素的中心正好落在 4 个源纹素的交点上,中心那次双线性采样就平均了 4 个纹素
Assets/Cookbook/Ch26/KawaseBlur.shader(节选:dual)
// Dual Kawase 下采样:中心权重 4 + 四个对角各 1,共 5 次采样,输出到一半分辨率
half4 FragDown(Varyings input) : SV_Target
{
    UNITY_SETUP_STEREO_EYE_INDEX_POST_VERTEX(input);
    float2 uv = input.texcoord;
    float2 h = _BlitTexture_TexelSize.xy * 0.5 * _KawaseOffset;
    half4 c = Tap(uv) * 4.0;
    c += Tap(uv - h);
    c += Tap(uv + h);
    c += Tap(uv + float2(h.x, -h.y));
    c += Tap(uv - float2(h.x, -h.y));
    return c * (1.0 / 8.0);
}

// Dual Kawase 上采样:菱形 4 点(权重 1)+ 对角 4 点(权重 2),共 8 次采样,输出到两倍分辨率
half4 FragUp(Varyings input) : SV_Target
{
    UNITY_SETUP_STEREO_EYE_INDEX_POST_VERTEX(input);
    float2 uv = input.texcoord;
    float2 h = _BlitTexture_TexelSize.xy * 0.5 * _KawaseOffset;
    half4 c = Tap(uv + float2(-h.x * 2.0, 0.0));
    c += Tap(uv + float2( h.x * 2.0, 0.0));
    c += Tap(uv + float2(0.0, -h.y * 2.0));
    c += Tap(uv + float2(0.0,  h.y * 2.0));
    c += Tap(uv + float2(-h.x,  h.y)) * 2.0;
    c += Tap(uv + float2( h.x,  h.y)) * 2.0;
    c += Tap(uv + float2( h.x, -h.y)) * 2.0;
    c += Tap(uv + float2(-h.x, -h.y)) * 2.0;
    return c * (1.0 / 12.0);
}
// Dual Kawase 下采样:中心权重 4 + 四个对角各 1,共 5 次采样,输出到一半分辨率
vec4 FragDown()
{
    vec2 uv = texcoord;
    vec2 h = _BlitTexture_TexelSize.xy * 0.5 * _KawaseOffset;
    vec4 c = Tap(uv) * 4.0;
    c += Tap(uv - h);
    c += Tap(uv + h);
    c += Tap(uv + vec2(h.x, -h.y));
    c += Tap(uv - vec2(h.x, -h.y));
    return c * (1.0 / 8.0);
}

// Dual Kawase 上采样:菱形 4 点(权重 1)+ 对角 4 点(权重 2),共 8 次采样,输出到两倍分辨率
vec4 FragUp()
{
    vec2 uv = texcoord;
    vec2 h = _BlitTexture_TexelSize.xy * 0.5 * _KawaseOffset;
    vec4 c = Tap(uv + vec2(-h.x * 2.0, 0.0));
    c += Tap(uv + vec2( h.x * 2.0, 0.0));
    c += Tap(uv + vec2(0.0, -h.y * 2.0));
    c += Tap(uv + vec2(0.0,  h.y * 2.0));
    c += Tap(uv + vec2(-h.x,  h.y)) * 2.0;
    c += Tap(uv + vec2( h.x,  h.y)) * 2.0;
    c += Tap(uv + vec2( h.x, -h.y)) * 2.0;
    c += Tap(uv + vec2(-h.x, -h.y)) * 2.0;
    return c * (1.0 / 12.0);
}
Assets/Cookbook/Ch26/KawaseBlur.cs(节选:dual)
void RenderDualKawase(CommandBuffer cmd)
{
    cmd.SetGlobalFloat(s_Offset, m_Offset);
    // 下行:全分辨率 → 1/2 → 1/4 …,每一步用 5 次采样的 Down 核
    RTHandle prev = cameraColor;
    for (int i = 1; i <= m_Iterations; i++)
    {
        Blitter.BlitCameraTexture(cmd, prev, m_Pyramid[i], material, PassDown);
        prev = m_Pyramid[i];
    }
    // 上行:逐级放大回去,每一步用 8 次采样的 Up 核,最后一步直接写回相机颜色
    for (int i = m_Iterations - 1; i >= 0; i--)
    {
        RTHandle dst = i == 0 ? cameraColor : m_Pyramid[i];
        Blitter.BlitCameraTexture(cmd, m_Pyramid[i + 1], dst, material, PassUp);
    }
}

经典 Kawase 的 C# 循环是“逐次参数必须用 cmd.SetGlobalFloat”的典型例子:每次迭代偏移不同,如果写成 material.SetFloat,5 次迭代全都会用第 5 次的偏移,效果完全不对,而且不会报任何错。

Assets/Cookbook/Ch26/KawaseBlur.cs(节选:kawase)
void RenderKawase(CommandBuffer cmd)
{
    Blitter.BlitCameraTexture(cmd, cameraColor, m_PingA, 0, true);
    var src = m_PingA; var dst = m_PingB;
    for (int i = 0; i < m_Iterations; i++)
    {
        // 每次迭代偏移不同:用 cmd.SetGlobalFloat 记录进命令缓冲。
        // 若用 material.SetFloat,所有迭代最终都会读到最后一次设置的值(见正文)
        cmd.SetGlobalFloat(s_Offset, (i + 0.5f) * m_Offset);
        Blitter.BlitCameraTexture(cmd, src, dst, material, PassKawase);
        (src, dst) = (dst, src);
    }
    Blitter.BlitCameraTexture(cmd, src, cameraColor, 0, true);
}
图 26-8左:经典 Kawase,半分辨率 5 次迭代;中:Dual Kawase 3 级(最低 1/8 分辨率);右:Dual Kawase 5 级(最低 1/32),半径大幅增加,开销只多了很小的两级
方法(1080p)每像素采样(全分辨率等效)半径增长特点
可分离高斯,1/2 分辨率,n 次迭代约 2 + 10n × ¼∝ √n最“正”,形状可控
Kawase,1/2 分辨率,n 次迭代约 2 + 4n × ¼∝ n实现最简单
Dual Kawase,k 级约 8 + 13 × (¼ + 1/16 + …) ≈ 12(最后一级上采样写全分辨率)∝ 2^k大半径最便宜,移动端首选

表中的采样数是按算法结构推算的量级,用来理解趋势;真实耗时取决于带宽、缓存和 GPU 架构,请以目标设备上的 Profiler 为准。