前言
两段思路:
- 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 选 Before 或 After 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)
从某个中心(屏幕中心或自定义点)向外沿射线多次采样并平均,营造速度感/眩晕/开镜外圈虚化。
要点:center、steps、strength 控制视觉;大半径/小步数易噪,配下采样/双线性可显著改善。
4. 抗噪与稳定性:从“颗粒感/闪烁”到丝滑
- 采样不足 + 步长过大 → 增加 taps / 减小 step;或采用分离高斯 + 下采样金字塔。
- 点采样导致颗粒 → 在自定义 HLSL 中显式使用双线性采样器(示例里用
BilinearTextureSampler0)。 - TAA/DLSS 交互 → 尝试切换到 After Tonemapping,或在 Pass 顺序上做调整。
- 越界采样 →
saturate(uv);或自定义镜像/包裹。 - 单 Pass 大核 → 不建议。改为分离 + 金字塔或采用迭代式方案(Kawase/Dual)。
- 大半径需求 → 从较低 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。
- 版本差异:若
SceneTextureLookup或GetPostProcessInputSize等接口报错,优先用材质节点 SceneTexture: PostProcessInput0 验证数据,再替换为 Custom。
7. 在 EUW/蓝图中做成可调控的工具面板
目标:一键用的“屏幕模糊控制面板”。
- 在模糊材质中暴露参数:
Radius/Sigma、Iterations/OffsetPixels(Kawase)、Center/Steps/Strength(径向)、BaseLOD/FineRadius/FineWeight。创建 MIC。 在 EUW 做一个
UMG面板:- 获取当前关卡的后处理体或相机的 Blendables。
- 列表展示所有模糊 MIC,提供启用/禁用切换。
- 滑条:半径、强度、步数、LOD;
Center支持“点击视口取点”。 - 下拉:Blendable Location(Before/After Tonemapping)。
- 预设按钮:
UI 虚化/冲刺速度感/过场淡化,一键写入一组参数。
补充:若项目用 Modular PP Pipeline,可把模糊打成独立 Pass(含 Downsample/Upsample),EUW 仅切换与参数化。
8. 调试与排错清单
- 在屏幕上叠加调试环/棋盘,检查半径与扩散是否均匀。
- 大半径却颗粒 → 增加采样、启用双线性、从低 LOD 取样、或改走金字塔。
- 出现闪烁/抖动 → 检查与 TAA/DLSS 的交互,尝试 After Tonemapping。
- 边缘黑边 →
saturate(uv)或镜像/包裹。 - 材质不生效 → 材质域是否为 Post Process;是否已把材质加入 Volume/Camera 的 Blendables;是否有哑 SceneTexture 节点启用宏。
9. 附录:节点版(无自定义 HLSL)实现思路
分离高斯(节点):
SceneTexture: PostProcessInput0→ 多个TextureSample(设置 Bilinear)。uv + ±(px, 0)、uv + ±(0, px)等偏移 →Add/Multiply组合。- 用
ScalarParameter控制半径/步长,AppendVector组织偏移;最后Weighted Add并除以权重和。
Kawase(节点):
- 小偏移四点取样(↗ ↖ ↘ ↙)求平均。
- 多次迭代:串 N 个
MaterialFunction或Custom包装的节点函数。
径向模糊(节点):
- 计算方向
dir = normalize(center - uv)。 - 多个
uv + t*dir*strength的采样点累加平均。
- 计算方向
若需要进一步模板化,可把上述组合封装为Material Function(MF),并提供MF_RadialBlur,MF_GaussianSeparableH/V,MF_KawaseIter等,统一参数命名。
还有一个奇怪的彩蛋..