MENU

• 2025 年 10 月 24 日 • 已有 45 只咪围观过 • -学海拾贝-

前言

两段思路:

  • 5×5 固定核的“直接采样 + 权重相加”的高斯近似。
  • “只保留 UV 不取颜色”的尝试。

关键澄清:模糊的本质是对“颜色场(或其他可平均的场)做加权平均”。如果“只返回 UV 偏移”,实际上只是移动取样位置,不会产生模糊,而是错位/拉伸/畸变。想要真正的模糊,必须基于偏移后的 UV 去采样场景颜色(或已渲染的中间贴图),并做加权累加与归一化。

结论:把“只要 UV”的想法改成“偏移 UV 后采样颜色并加权”,才是正确的屏幕模糊路径。

基本是跟着kodeco中的教程中引发的思考。


2. 屏幕空间采样的关键知识

  • 采样源:通常选择上一阶段的场景颜色(PostProcessInput0 / SceneColor)。在 Custom 节点中可使用 GetDefaultSceneTextureUV(...) 获取稳定的屏幕 UV,再用 SceneTextureLookup(...)Texture2DSample(...) 取色。
  • 像素尺寸/步长(Texel Size):用 GetPostProcessInputSize(0).zw 得到 (1/Width, 1/Height),把“像素偏移”换成 UV 偏移。
  • 材质域与混合位置:Post Process 材质域;Blendable Location 选 BeforeAfter Tonemapping 会影响观感与稳定性。经验上 After Tonemapping 在多数项目里更稳、更省,Before 可避免某些 TAA/GBuffer 交互问题。
  • 越界处理:大半径时请 saturate(uv) 或按需求改为镜像/包裹以避免边缘黑边。

3. 三类核心实现(分离高斯 / Kawase / 径向模糊)

3.1 分离高斯(Separable Gaussian)

把 N×N 的核拆成 水平 N 次 + 垂直 N 次,采样数从 N² 降到 2N。常与下采样金字塔(1/2、1/4 分辨率)配合:在低分辨率上模糊、再回读插值,更干净、更省算力

适用:全屏柔和景深感、UI 背景虚化、过场淡入淡出背景。
优点:质量稳定,易控半径;配金字塔后性价比高。
缺点:需要两 Pass(或两次 Custom),管线稍复杂。


3.2 Kawase / Dual Filter

迭代式模糊:每一轮使用较小偏移,以多次 Pass 累积朦胧感。

适用:追求更低成本、风格化的虚化。
优点:实现简单,开销小。
缺点:频响与高斯不同,边缘特性略“油”。


3.3 径向模糊(Radial Blur)

从某个中心(屏幕中心或自定义点)向外沿射线多次采样并平均,营造速度感/眩晕/开镜外圈虚化

要点centerstepsstrength 控制视觉;大半径/小步数易噪,配下采样/双线性可显著改善。


4. 抗噪与稳定性:从“颗粒感/闪烁”到丝滑

  1. 采样不足 + 步长过大 → 增加 taps / 减小 step;或采用分离高斯 + 下采样金字塔
  2. 点采样导致颗粒 → 在自定义 HLSL 中显式使用双线性采样器(示例里用 BilinearTextureSampler0)。
  3. TAA/DLSS 交互 → 尝试切换到 After Tonemapping,或在 Pass 顺序上做调整。
  4. 越界采样saturate(uv);或自定义镜像/包裹。
  5. 单 Pass 大核 → 不建议。改为分离 + 金字塔或采用迭代式方案(Kawase/Dual)。
  6. 大半径需求 → 从较低 LOD读取(Texture2DSampleLevel)可明显降噪。

5. 可复制 HLSL 模板(Custom 节点)

说明:以下代码用于Post Process 材质域Custom 节点。建议在材质中放一个 “SceneTexture: PostProcessInput0” 作为哑输入,以确保资源绑定;某些版本差异下,若宏或采样器名与工程不一致,请按照注释切换到另一种写法。

5.1 直接 5×5 高斯(演示/小半径)

// 输入:BlurRadius(建议 2~4),Range(像素步长)
float2 uv = GetDefaultSceneTextureUV(Parameters, 14);
float2 px = GetPostProcessInputSize(0).zw; // (1/Width, 1/Height)

static const float kernel[25] = {
  0.01, 0.02, 0.04, 0.02, 0.01,
  0.02, 0.04, 0.08, 0.04, 0.02,
  0.04, 0.08, 0.16, 0.08, 0.04,
  0.02, 0.04, 0.08, 0.04, 0.02,
  0.01, 0.02, 0.04, 0.02, 0.01
};

float stepPix = Range;              // 以“像素”为单位的步长
float2 stepUV = px * stepPix;

float3 sum = 0;
int idx = 0;

[unroll]
for (int y = -2; y <= 2; ++y)
{
    [unroll]
    for (int x = -2; x <= 2; ++x)
    {
        float2 o = float2(x, y) * stepUV;
        float3 c = Texture2DSample(PostprocessInput0, BilinearTextureSampler0, saturate(uv + o)).rgb;
        sum += c * kernel[idx++];
    }
}
return float4(sum, 1);
注:BilinearTextureSampler0 若在你版本中不可用,改用 SceneTextureLookup(saturate(uv + o), 14, /*Filtered=*/true).rgb 也可(不同版本宏名略有差异)。

这里我尝试了不同的SceneTextureLookup,有一个感觉很像热成像,挺有意思的


5.2 分离高斯 · 水平 Pass

// Inputs: Radius (像素半径), Sigma (标准差)
float2 uv  = GetDefaultSceneTextureUV(Parameters, 14);
float2 px  = GetPostProcessInputSize(0).zw;
float  r   = max(1.0, Radius);
float  sigma = max(0.001, Sigma);

int    K = (int)r;
float  wsum = 0;
float3 sum  = 0;

[loop]
for (int i = -K; i <= K; ++i)
{
    float w = exp(-0.5 * (i*i) / (sigma*sigma));
    float2 o = float2(i, 0) * px;
    float3 c = Texture2DSample(PostprocessInput0, BilinearTextureSampler0, saturate(uv + o)).rgb;
    sum  += c * w;
    wsum += w;
}
return float4(sum / max(wsum, 1e-5), 1);

5.3 分离高斯 · 垂直 Pass

// 与水平 Pass 相同,把偏移从 (i, 0) 改为 (0, i)
float2 uv  = GetDefaultSceneTextureUV(Parameters, 14);
float2 px  = GetPostProcessInputSize(0).zw;
float  r   = max(1.0, Radius);
float  sigma = max(0.001, Sigma);

int    K = (int)r;
float  wsum = 0;
float3 sum  = 0;

[loop]
for (int i = -K; i <= K; ++i)
{
    float w = exp(-0.5 * (i*i) / (sigma*sigma));
    float2 o = float2(0, i) * px;
    float3 c = Texture2DSample(PostprocessInput0, BilinearTextureSampler0, saturate(uv + o)).rgb;
    sum  += c * w;
    wsum += w;
}
return float4(sum / max(wsum, 1e-5), 1);

5.4 Kawase(双线性 + 迭代式)

// Inputs: Iterations, OffsetPixels
float2 uv = GetDefaultSceneTextureUV(Parameters, 14);
float2 px = GetPostProcessInputSize(0).zw;
int    iters = max(1, (int)Iterations);
float  offPx = max(0.5, OffsetPixels);

float3 col = Texture2DSample(PostprocessInput0, BilinearTextureSampler0, uv).rgb;

[loop]
for (int i = 0; i < iters; ++i)
{
    float2 o = px * (offPx + i * 0.5);
    float3 s0 = Texture2DSample(PostprocessInput0, BilinearTextureSampler0, saturate(uv + float2( o.x,  o.y))).rgb;
    float3 s1 = Texture2DSample(PostprocessInput0, BilinearTextureSampler0, saturate(uv + float2(-o.x,  o.y))).rgb;
    float3 s2 = Texture2DSample(PostprocessInput0, BilinearTextureSampler0, saturate(uv + float2( o.x, -o.y))).rgb;
    float3 s3 = Texture2DSample(PostprocessInput0, BilinearTextureSampler0, saturate(uv + float2(-o.x, -o.y))).rgb;
    col = (col + s0 + s1 + s2 + s3) * 0.2; // 简单平均,可加权
}
return float4(col, 1);

5.5 径向模糊(Radial Blur)

// Inputs: Center(float2, 0~1), Steps, Strength(0~1)
float2 uv = GetDefaultSceneTextureUV(Parameters, 14);

float2 center   = Center;
int    steps    = max(1, (int)Steps);
float  strength = Strength;

float3 acc = 0;
float  wsum = 0;

[loop]
for (int i = 1; i <= steps; ++i)
{
    float t = i / (float)steps;
    float2 suv = lerp(uv, center, t * strength);
    float  w   = 1; // 可换成 t 或其他分布权重
    float3 c   = Texture2DSample(PostprocessInput0, BilinearTextureSampler0, saturate(suv)).rgb;
    acc  += c * w;
    wsum += w;
}
return float4(acc / max(wsum, 1e-5), 1);

5.6 大半径降噪:低 LOD 预滤再细采样

// Inputs: BaseLOD(>=0), FineRadius, FineWeight(0~1)
float2 uv = GetDefaultSceneTextureUV(Parameters, 14);
float2 px = GetPostProcessInputSize(0).zw;

float  baseLod   = max(0, BaseLOD);
float3 baseColor = Texture2DSampleLevel(PostprocessInput0, BilinearTextureSampler0, uv, baseLod).rgb;

int    K = (int)FineRadius;
float3 sum = 0; float wsum = 0;

[loop]
for (int i = -K; i <= K; ++i)
{
    float w = 1.0 / (1 + abs(i));
    float2 o = float2(i, 0) * px * 2; // 少量细采样(水平为例)
    float3 c = Texture2DSampleLevel(PostprocessInput0, BilinearTextureSampler0, saturate(uv + o), baseLod).rgb;
    sum += c * w; wsum += w;
}
float3 fine = sum / max(wsum, 1e-5);
float3 outc = lerp(baseColor, fine, FineWeight);
return float4(outc, 1);

降噪还是必要的,不降噪可能是这样的:


6. 性能与平台注意事项

  • 分离高斯 vs 单次 NxN:分离将采样数从 N² → 2N;半径稍大时必须拆。
  • 下采样金字塔:在 1/2、1/4 分辨率上模糊,最后回读插值,更干净更省
  • 过滤与采样器:显式双线性采样(或各平台可用的等效过滤)能显著降噪。
  • 移动端:部分移动路径对 SceneTexture 支持有限,必要时改用自定义 RT 与自定义 Pass。
  • 版本差异:若 SceneTextureLookupGetPostProcessInputSize 等接口报错,优先用材质节点 SceneTexture: PostProcessInput0 验证数据,再替换为 Custom。

7. 在 EUW/蓝图中做成可调控的工具面板

目标:一键用的“屏幕模糊控制面板”。

  • 在模糊材质中暴露参数:Radius/SigmaIterations/OffsetPixels(Kawase)、Center/Steps/Strength(径向)、BaseLOD/FineRadius/FineWeight。创建 MIC
  • EUW 做一个 UMG 面板:

    1. 获取当前关卡的后处理体或相机的 Blendables
    2. 列表展示所有模糊 MIC,提供启用/禁用切换。
    3. 滑条:半径、强度、步数、LOD;Center 支持“点击视口取点”。
    4. 下拉:Blendable Location(Before/After Tonemapping)。
    5. 预设按钮:UI 虚化 / 冲刺速度感 / 过场淡化,一键写入一组参数。
补充:若项目用 Modular PP Pipeline,可把模糊打成独立 Pass(含 Downsample/Upsample),EUW 仅切换与参数化。

8. 调试与排错清单

  • 在屏幕上叠加调试环/棋盘,检查半径与扩散是否均匀。
  • 大半径却颗粒 → 增加采样、启用双线性、从低 LOD 取样、或改走金字塔。
  • 出现闪烁/抖动 → 检查与 TAA/DLSS 的交互,尝试 After Tonemapping。
  • 边缘黑边saturate(uv) 或镜像/包裹。
  • 材质不生效 → 材质域是否为 Post Process;是否已把材质加入 Volume/Camera 的 Blendables;是否有哑 SceneTexture 节点启用宏。

9. 附录:节点版(无自定义 HLSL)实现思路

  • 分离高斯(节点)

    1. SceneTexture: PostProcessInput0 → 多个 TextureSample(设置 Bilinear)。
    2. uv + ±(px, 0)uv + ±(0, px) 等偏移 → Add/Multiply 组合。
    3. ScalarParameter 控制半径/步长,AppendVector 组织偏移;最后 Weighted Add 并除以权重和。
  • Kawase(节点)

    1. 小偏移四点取样(↗ ↖ ↘ ↙)求平均。
    2. 多次迭代:串 N 个 MaterialFunctionCustom 包装的节点函数。
  • 径向模糊(节点)

    1. 计算方向 dir = normalize(center - uv)
    2. 多个 uv + t*dir*strength 的采样点累加平均。
若需要进一步模板化,可把上述组合封装为 Material Function(MF),并提供 MF_RadialBlur, MF_GaussianSeparableH/V, MF_KawaseIter 等,统一参数命名。

还有一个奇怪的彩蛋..

返回文章列表 打赏
本页链接的二维码
打赏二维码