1. 先把“毛玻璃”拆开:实时渲染里它其实是三件事
做“虚幻带法线效果的毛玻璃”这个效果,最耗时间的从来不是连线,而是想清楚你到底要什么。我第一次接到这个需求是在一个室内场景项目里,客户只说了一句“浴室门要那种磨砂玻璃,看不清人但有轮廓”,我当场觉得这有什么难的,结果前后返工了三版才落地。原因很简单:磨砂玻璃在现实里同时干了三件互不相关的事——把透过来的光打散、让背后物体的边界模糊、让表面有细微的不规则起伏。实时渲染里这三件事分别对应不同的管线能力,混在一起调就一定会互相打架。
1.1 磨砂玻璃的两种光学行为,以及实时渲染为什么只能“假装”
物理上,磨砂玻璃就是用酸蚀或喷砂在表面制造一层粗糙的微结构。光线穿过时,每个微小的凹凸面都会让入射光产生一个微小的方向偏折,宏观上呈现出两个结果:一是透射图像的高频细节被抹掉,也就是模糊;二是透过来的光有一定的方向偏移和散射,也就是你在斜着看玻璃的时候,后面的东西会“歪”一点。
关键词里“带法线效果”指的正是第二件事的实现手段——用一张法线贴图描述这些微观凹凸,然后拿法线的 xy 分量去偏移采样坐标。但这里有个根本性的限制:真正的磨砂玻璃是让光线在介质内部散射几十上百次,而实时渲染里我们只能拿到一张已经渲染好的屏幕图像(SceneColor),对着它做二维的重采样。这意味着我们做的是“屏幕空间伪折射”,它没有厚度,不考虑背光透射的衰减,也不会因为你换视角而重新计算光路。想清楚这一点,你就不会再去纠结“为什么我的毛玻璃没有那种通透的散光感”——那不是参数问题,是模型层面的缺失,需要额外的体积散射或光照透射(Translucency Lighting)去补。
所以我在项目里给自己定的第一原则是:模糊靠多次采样 SceneColor 堆,法线负责扰动采样点位置和后续的光照法线,两者职责分开,各自能独立调。
1.2 虚幻半透明管线给的三个抓手:Opacity、Refraction、SceneColor
虚幻的半透明材质通道不算多,但每个都能玩出花,先把能用的三个抓手记牢:
- Opacity:透明度。毛玻璃不是全透明,通常给 0.85~0.98 这种接近不透明的值,让色调和模糊感主导,同时保留一点点背后物体的层次。
- Refraction:折射率偏移。默认输入 1.0 表示不折射,输入一个接近 1.0 的值(比如 0.98~1.02)会产生屏幕空间偏移。这一路最省,但只能让图像“动起来”,不会变模糊。
- SceneColor(通过 SceneTexture: PostProcessInput0 拿到):这才是真模糊的本钱。你可以把它当普通贴图多次采样,做任意形状的卷积。
这三者不是三选一的问题。我的常用组合是:SceneColor 做模糊主体,法线扰动模糊中心点,Refraction 给一点点整体偏移做“厚度感”。三层叠加的效果比任何单独一层都自然,因为真实玻璃的观感本来就不是单一来源。
提示:SceneTexture 节点在半透明材质里能采样到的是当前帧已渲染的不透明物体,不包含其他半透明物体。如果你需要两块毛玻璃互相遮挡,得走额外的 SceneCapture 或后处理通道,别在这个材质里死磕。
1.3 一个常见误区:只挂个法线贴图不叫毛玻璃
网上大量教程讲“毛玻璃”,最后给出的方案是把法线贴图接进 Normal 引脚,然后把 Refraction 设成 1.02。这个方案跑起来效果是这样的:玻璃对面那棵树被扭曲成波浪形,但树的叶子依然根根分明,一点磨砂感都没有——因为它只是把图像挪了个位置,压根没做任何频率削减。
我在一个 Demo 里被这种效果坑过整整一天。当时以为是法线强度不够,往死里加,加到 0.3,结果画面开始撕裂、边缘拉出长长的黑边,问题反而更严重。后来才想明白:模糊和扰动是两件事,模糊必须靠采样数解决。法线贴图能控制的只有“扰动”,它永远不会给你磨砂的视觉效果,除非你把它当作采样偏移的方向图去用。
这一点想通了,后面的实现路径就清晰了:SceneColor 多次采样(负责模糊)+ 法线贴图生成偏移量(负责不规则扰动)+ 采样点分布(负责噪点和性能的平衡)。三个变量,各自有明确的调参区间。
2. 材质骨架怎么搭:从新建材质到第一版能跑的玻璃
材质骨架这一步,很多坑是在细节面板里埋下的,而不是在节点连线上。我见过不止一个人把节点连得完全正确,但画面黑的、白的、透明的什么都出来了,最后发现是某个勾选项没开。
2.1 Material Domain / Blend Mode / Lighting Mode 的选型逻辑
先说结论,可以直接抄:
| 设置项 | 取值 | 为什么 |
|---|---|---|
| Material Domain | Surface | 场景中的实体玻璃必须走 Surface,UI 域拿不到正确的深度和法线信息 |
| Blend Mode | Translucent | 不透明混合拿不到 SceneColor 采样能力 |
| Shading Model | Default Lit | 需要受光照影响,Unlit 会让玻璃在暗处“发光” |
| Lighting Mode | Surface Translucency Volume / Volumetric NonDirectional | 前者质量好、开销大;后者便宜、低频光照会丢失 |
| Two Sided | 视情况勾选 | 双面玻璃必须开,单面贴墙的可以关掉省一半 |
| Translucency Pass | Before DOF | 关键项,详见下文 |
| Allow Scene Texture Fetch | 视引擎版本 | 某些版本需要手动开启才能采样 PostProcessInput0 |
Translucency Pass这个选项极易被忽略。它决定了半透明物体在渲染队列里的位置:选 After DOF,材质采样到的 SceneColor 是已经做过景深的,模糊层会和景深打架,边缘会出现一圈“糊掉的光晕”;选 Before DOF,采样到的是清晰的不透明层,模糊结果干净,代价是这块玻璃自身不会被景深模糊(远处看会显得太锐)。我的处理习惯是:主要交互距离近的玻璃用 Before DOF,远景装饰玻璃用 After DOF 并接受轻微光晕。
Lighting Mode的选择则直接影响“法线效果”能不能被看见。Surface Translucency Volume 会为半透明物体生成一个体积光照网格,法线参与光照计算,玻璃表面的起伏会有真实的高光;Volumetric NonDirectional 只保留环境光照的强度,法线只影响折射不影响高光。如果你的毛玻璃主要靠反光塑造质感,别省这个开销。
2.2 屏幕 UV 的正确来源与边缘裁剪
模糊采样的坐标必须是屏幕空间 UV,这一点没有商量余地。场景里有几个节点看起来都能用,实际差别很大:
- ScreenPosition:输出屏幕空间的 UV,注意它默认是带视口缩放的,直接拿去采样 SceneColor 一般是正确的。
- TextureCoordinate[0]:模型自身的 UV,用于采样法线贴图是对的,用于采样屏幕颜色是错的。
- ViewportUV:一般等价于 ScreenPosition 的一部分,做后处理材质时用得多。
我踩过的坑是:把TextureCoordinate拿去当采样坐标,结果玻璃上出现了模型 UV 展开图的花纹,看起来像是贴图错位,实际上是坐标系完全用错了。这个错误很隐蔽,因为在正对玻璃的时候,ScreenPosition 和 TextureCoordinate 在数值上都接近 0~1,看不出区别,一旦转视角就崩了。
除了用对坐标系,还有一件必做的事:边缘裁剪。原因很现实——法线扰动会把采样坐标推出玻璃自身的屏幕投影范围,推到玻璃外面的时候,采样到的就是玻璃旁边的空气或者墙壁的颜色,形成一圈明显的“拉伸”。处理办法有三种,我一般混用:
- 在材质里对扰动后的 UV 做
Clamp,限制在屏幕 0~1 范围内,防止采到画面外。 - 用顶点法线和视线方向做点积,得到菲涅尔遮罩,在边缘处把扰动强度和模糊半径淡出到 0。这是最自然的一招。
- 用
Depth Fade让玻璃和背后物体距离很近时整体透明度降低,减少拉伸被看到的概率。
第 2 条的具体做法:拿PixelNormalWS和CameraVectorWS做点积,取绝对值后做Saturate,再做一个Power(指数 1.5~3)压一下曲线,得到边缘 → 中心从 0 到 1 的渐变遮罩,乘到法线扰动强度上。这样玻璃正中间磨砂感最强,越靠近轮廓越清晰,非常符合真实玻璃透过厚度的表现。
2.3 法线扰动向量的三种生成方式与取舍
法线贴图是这件事的核心资产,但并不是只能用“一张图”。实际项目里我常用的有三种:
第一种,真实法线贴图采样。找一张磨砂质感或者磨砂玻璃颗粒的 Normal Map,用模型 UV 采样,取 xy 乘以强度。优点是可控、可美术化、能做出不同的磨砂颗粒风格(细砂、粗砂、橘皮纹)。缺点是法线贴图在远处会因为 mip 逐级平均而趋近 (0.5, 0.5, 1),扰动幅度衰减到几乎为零,磨砂感在远景消失。
第二种,程序化噪声。用两层不同频率的噪声(比如对TextureCoordinate做Noise节点,或者用两个不同平铺的纹理叠乘)组合出一个高低频混合的扰动图。优点是永远不会因为没有 mip 而衰减,可以在任意距离保持扰动;缺点是高频噪声在远处会闪烁,必须自己控制频率和远处衰减。
第三种,预烘焙到顶点色或 UV2。适合玻璃形态固定的场景(比如大量重复的浴室隔断),把扰动图案烘到 UV2 上,运行时零采样成本。缺点是没法动态改。
我在两个项目里做过对比:室内近景用第一种加手动 mip bias 控制;大场景远景批量玻璃用第二种,并且按相机距离做扰动衰减。如果把两种混用在同一块玻璃上,注意它们的强度要分别调,否则近看是橘皮纹、远看是细噪点,颗粒感断裂得很难看。
注意:法线贴图用模型 UV 采样,扰动偏移应用到屏幕 UV 上——这两个坐标系千万别混。我第一版就是把法线贴图的 UV 也用 ScreenPosition 采的,结果玻璃一移动,磨砂纹路跟着屏幕滑动,看起来像整块玻璃在“流汗”。
3. 模糊内核:采样数、半径和法线强度怎么配
到了核心部分。毛玻璃的观感好坏,八成取决于这三个参数的配合,而它们之间是有数学关系的,不是靠手感瞎调。
3.1 黄金角螺旋采样与采样数下限的推导
最朴素的模糊写法是在中心点周围取 N 个固定位置的点求平均。如果这 N 个点的位置是规则的网格,你会看到明显的方形块或十字条纹;如果随机取,每帧位置不同,画面会抖。所以常用的是低差异序列采样,我习惯用黄金角螺旋(Golden Angle Spiral):
// 黄金角,弧度值 static const float GOLDEN_ANGLE = 2.39996323; float radius = BlurRadiusPx; for (int i = 0; i < SampleCount; i++) { float r = sqrt((i + 0.5) / SampleCount) * radius; float theta = i * GOLDEN_ANGLE; float2 offset = float2(cos(theta), sin(theta)) * r; // 采样... }sqrt(i/N)这个开方是必须的,它把采样点从中心向外均匀铺开。不加开方,采样点会往中心挤,模糊看起来是一个亮核外面一圈毛边。
关键来了,采样数到底要多少?有个简单的几何推导:N 个点在半径 R 的圆内均匀分布,平均每个点占据的面积是 πR²/N,等效间距约为 R/√N(忽略了常数项)。当这个间距大于 1 个屏幕像素时,相邻采样点之间就留下了空档,画面会出现颗粒状的带状伪影。
于是得到一个下限公式:
N ≥ R²
反过来说,R 每翻一倍,采样数要翻四倍。这就是为什么“把模糊半径调大一点”这么贵:
| 模糊半径 R(像素) | 理论最少采样数 N≈R² | 实际常用值 | 适用场景 |
|---|---|---|---|
| 4 | 16 | 8~12 | 玻璃隔断、贴脸近景 |
| 8 | 64 | 24~32 | 常规毛玻璃 |
| 16 | 256 | 48~64 | 厚磨砂、浴室门 |
| 32 | 1024 | 96~128 | 只建议特写镜头 |
实践中允许采样数低于理论值,因为有 TAA 和运动模糊帮忙掩盖噪点,经验系数取 2~4 倍降级(也就是 N ≈ R²/3)。但如果你的场景是静态视角的展示图或者用的是无抗锯齿的渲染,这条捷径走不通,必须老实堆到 R²。
3.2 法线扰动加在中心还是加在每个采样点上
这个问题决定了你最终得到的是“水波纹玻璃”还是“磨砂玻璃”,值得单独说。
方案 A:只在模糊中心加扰动。采样偏移量 = 法线 xy × 强度,加到中心 UV 上,然后围绕这个新中心做模糊。效果是整块玻璃像一个被轻轻扭曲的镜头,边缘有流动感,磨砂颗粒是均匀的。
方案 B:每个采样点各自扰动。每个采样点的 UV 再加上法线 xy × 强度。效果是采样点分布被打乱,产生更强烈的颗粒感,但代价是采样点可能被挤到一起,出现局部采样不足的噪点。
我的结论是:法线强度小的时候用 B,大的时候用 A。具体来说,如果扰动幅度小于模糊半径的一半,用 B 能让颗粒更生动;如果扰动幅度接近或超过模糊半径,用 B 会让采样点严重挤压,噪点压不住,必须退回 A。也可以折中——用 A 做基础位移,再把法线的一部分(比如 30%)加在采样点上,两者混合,颗粒感和稳定性兼顾。
这里顺带一个容易被忽视的点:法线扰动的量纲。它偏移的是屏幕 UV,范围 0~1。法线贴图采样出来的 xy 是 -1~1,所以强度参数的含义是“最大偏移占屏幕宽度的比例”。强度 0.01 意味着最大偏移 1% 屏幕宽度,1080p 下大约 19 像素。这个数值远超大多数人的直觉——很多教程让人填 0.1 甚至 1,那当然是满屏撕裂。我的经验区间是0.001~0.01,对应 2 到 19 像素的扰动。
| NormalStrength | 1080p 下最大偏移 | 观感 |
|---|---|---|
| 0.0005 | 约 1 px | 几乎看不出,只在高光处有细微变化 |
| 0.002 | 约 4 px | 细腻磨砂,最佳通用值 |
| 0.005 | 约 10 px | 明显的凹凸感,适合橘皮纹玻璃 |
| 0.01 | 约 19 px | 冰裂纹/水波纹,注意边缘遮罩 |
| >0.02 | >38 px | 一定穿帮,除非专门做扭曲特效 |
3.3 Custom 节点的完整 HLSL 与参数接线
节点图连线太啰嗦,我直接把内核写成一个 Custom 节点。这样做的好处是循环次数可控、能自己控制采样细节,也方便后面做平台分支。
Custom 节点的输入引脚这样配:
SceneColorTex(Texture2D)NormalTex(Texture2D)ScreenUV(float2)UVScale(float)BlurRadiusPx(float)NormalStrength(float)SampleCount(float,用静态整数分档)ViewSizeInv(float2,屏幕尺寸倒数)
输出类型float3。核心代码:
// ---- 参数预处理 ---- float2 texel = ViewSizeInv; float radius = max(BlurRadiusPx, 0.0); // ---- 法线扰动:物体UV采样,屏幕UV应用 ---- float3 n = Texture2DSample(NormalTex, NormalTexSampler, ScreenUV /*占位,实际用模型UV*/).rgb; n = n * 2.0 - 1.0; float2 distort = n.xy * NormalStrength; float2 centerUV = ScreenUV + distort; // ---- 黄金角螺旋抖动采样 ---- static const float GOLDEN_ANGLE = 2.39996323; float3 accum = 0.0; [unroll] for (int i = 0; i < 32; i++) { float fi = (float)i + 0.5; float r = sqrt(fi / 32.0) * radius * texel.x; float th = fi * GOLDEN_ANGLE; float2 off = float2(cos(th), sin(th)) * r; // 采样点再叠一层弱扰动,制造颗粒 float2 suv = centerUV + off + distort * 0.3; accum += Texture2DSample(SceneColorTex, SceneColorTexSampler, suv).rgb; } return accum / 32.0;几个必须注意的写法细节:
- 循环次数写成字面量(上面的 32),不要用参数变量。用参数会强制编译器生成动态循环,性能会掉一个台阶,而且部分平台直接不支持。
- 用
Texture2DSample宏而不是.Sample(),因为宏内部会处理好采样器绑定,兼容性好。 texel.x和texel.y在非正方形分辨率下需要分开乘,不要图省事只用 x,否则在宽屏上模糊会变成椭圆。- 法线采样要用模型 UV(
TextureCoordinate[0] * UVScale),上面的代码里用 ScreenUV 只是示意位置,实际接线请换成物体 UV。
如果要分档,最简单的做法是材质里放一个Static Switch Parameter,三档分别接 16 / 32 / 64 三个 Custom 节点,或者用材质实例的静态开关在打包时裁掉用不到的分支。动态的if在像素着色器里也是要付代价的,能静态化就静态化。
3.4 把模糊做“稳”:双线性、Mip、双面与深度淡出
代码跑起来之后,通常会立刻遇到四个问题,我按出现频率排:
第一个,块状感。采样 SceneColor 时如果用点采样,模糊结果会出现明显的方块。原因是屏幕纹理的采样器过滤模式可能不是线性的。解决办法有两个:一是确认采样器状态,二是自己手动做双线性——对每个采样点取四个角做插值。第二种更贵,一般先用第一种。
第二个,远景颗粒消失。法线贴图有 mip,远处采样到的是平均色 (0.5, 0.5, 1),去偏移之后等于 (0, 0, 0),扰动归零。如果你希望远处保留颗粒感,可以给法线贴图采样节点设置MipValueMode = MipLevel并锁定在 mip 0 或 1;反过来,如果远景出现摩尔纹闪烁,就反过来限制最大 mip。
第三个,双面玻璃法线方向反了。从背面看,法线贴图的扰动应该反向才符合光学直觉。用TwoSidedSign节点乘到法线的 xy 上即可——这个节点在光栅化时根据三角形朝向输出 +1 或 -1,一行搞定。
第四个,边缘发散。前面 2.2 提到过,用菲涅尔遮罩淡出扰动强度,同时把模糊半径也一起乘遮罩。这个我做错过一次,只遮了扰动没遮半径,结果边缘虽然不撕裂,但会有一圈“糊得厉害”的亮边,特别在逆光时非常明显。
再加一个Depth Fade,参数FadeDistance给 5~20 单位,让玻璃和背后物体几乎贴上的时候透明度自动降低,能显著减少近距离拉伸。这个东西在狭窄的浴室、走廊场景里几乎是必备的,能救回一大半穿帮镜头。
4. 平台适配与性能账本
毛玻璃是典型的“效果一眼惊艳、性能一算心惊”的东西,因为它没有 LOD 的天然退路,采样数说多少就是多少。
4.1 移动端和前向渲染的限制与替代做法
先说最硬的限制:移动端和部分前向渲染路径下,半透明材质对 SceneColor 的访问能力是受限的。不是完全不能做,但通常需要额外的 RT 拷贝(把不透明结果复制到一张纹理里),这个拷贝本身就是一次全屏带宽消耗,而且在不同渲染路径下的支持程度不一样。
所以移动端的现实选择是:放弃真模糊,回到 Refraction + 法线扰动的方案,接受“只有扭曲没有磨砂”的效果。这个方案在移动端几乎是免费的多边形填充开销,只要法线贴图分辨率不太离谱。
如果产品经理坚持要在移动端要模糊感,我给三个折中方案:
- 预烘焙模糊层:把玻璃后面的背景烘焙成一张低分辨率贴图,玻璃材质直接采样这张贴图做模糊。适合背景静态、镜头固定的场景(比如展示厅的固定视角)。
- 降低分辨率做模糊:把 SceneColor 采样降到一个低分辨率 RT 上做模糊,再通过材质采样回来。这需要代码侧配合,不是纯材质能解决的。
- 限制区域:只有玩家靠近的玻璃才开高采样,远的用 Refraction 版本。通过材质参数和蓝图动态切换。
前向渲染还有一个坑:Lighting Mode里的一些高级选项可能不被支持,材质会在编译时静默降级,效果变暗或者直接变黑。遇到“编译通过了但材质是黑的”,先查这个。
4.2 开销估算表与三档降级方案
算笔账心里才有底。1080p 是约 207 万像素,假设玻璃覆盖屏幕 30%(已经很夸张了),那就是约 62 万像素,每个像素的 SceneColor 采样次数就是总采样次数:
| 采样数 | 每次采样开销 | 覆盖 30% 屏幕的总采样 | 参考定位 |
|---|---|---|---|
| 8 | 1 fetch | 约 500 万 | 低配/VR 必备 |
| 16 | 1 fetch | 约 1000 万 | 移动端上限 |
| 32 | 1 fetch | 约 2000 万 | PC 主流 |
| 64 | 1 fetch | 约 4000 万 | PC 高配、特写 |
| 128 | 1 fetch | 约 8000 万 | 只用于单块特写玻璃 |
这个数量级本身不算爆炸(一个复杂的不透明材质也可能有十几次采样),但它有两个隐藏成本:一是采样是随机访问,缓存命中率低,实际带宽消耗比顺序访问高得多;二是Overdraw,玻璃叠玻璃的场景下开销是相乘的,三块全屏大小的玻璃叠在一起就是三倍。
我总结的三档降级配置:
| 档位 | 采样数 | 模糊半径 | 法线扰动 | 额外手段 |
|---|---|---|---|---|
| 低 | 8 | 4~6 px | 0.003 | 关闭菲涅尔淡出,改用简单 Clamp |
| 中 | 24~32 | 8~12 px | 0.002 | 菲涅尔遮罩 + Depth Fade |
| 高 | 48~64 | 16 px | 0.002 | 全功能 + 逐采样点扰动 |
在项目里我用Scalability分组和材质质量级别挂钩,让玩家自己在画质选项里切换,比偷偷降级然后被投诉“效果不一样”要体面得多。
提示:玻璃数量多的场景,务必用
Merge Actors或者在 LOD 里把远景玻璃换成简单的 Refraction 材质。我见过一个场景里放了 40 块独立毛玻璃,帧数直接腰斩,合并之后再按屏幕占比分档,救回来 20 帧。
5. 排障实录:那些让人怀疑人生的现象
这个效果我做过不下五遍,几乎每次都踩到新坑。下面这些是我真实遇到过并且记录下来的,按症状分类。
5.1 黑块、拉伸、边缘发散速查表
| 症状 | 最可能的原因 | 处理 |
|---|---|---|
| 整块玻璃是黑的 | SceneTexture 未开启 / Translucency Pass 设置错误 | 检查材质细节面板,改用 Before DOF 试一次 |
| 玻璃上出现模型 UV 花纹 | 采样坐标用了 TextureCoordinate 而非 ScreenPosition | 换节点,屏幕颜色必须用屏幕 UV 采 |
| 边缘一圈拉伸的黑边 | 法线扰动把 UV 推出玻璃范围 | 加菲涅尔淡出 + UV Clamp |
| 画面偶发闪白 | 采样坐标超出 0~1,采到了未初始化的边缘 | 采样前saturate(uv) |
| 磨砂颗粒随视角滑动 | 法线贴图用了屏幕 UV 采样 | 改成物体 UV |
| 远景颗粒消失 | 法线贴图 mip 平均化 | 锁 mip 0~1,或改用程序化噪声 |
| 从背面看扰动方向反了 | 双面材质未处理法线符号 | 乘TwoSidedSign |
| 玻璃和背后物体交界处有亮环 | Depth Fade 距离过小 / 未开启 | FadeDistance 拉到 10~20 |
| 移动端材质全黑 | 前向路径不支持 SceneColor 采样 | 改用 Refraction 方案 |
其中“整块玻璃是黑的”这个症状我遇到最多次,原因五花八门:有一次是Translucency Pass选了 After DOF;有一次是忘了给法线贴图采样器的 UV 做平铺;还有一次是材质实例覆盖了某个参数,把它设成了 0,导致采样半径变成零,所有采样点挤在同一个位置。最后一次最坑,因为材质本体完全正常,只在实例上出错,查了半天才发现。
5.2 与景深、TAA、反射打架的问题
和景深打架。前文提过,半透明物体在 DOF 之前或之后会对模糊结果产生不同的影响。如果玻璃在远处被景深模糊,而它自己又对 SceneColor 做了一次模糊,就成了“模糊的模糊”,会出现明显的低频色块。我的处理是把毛玻璃的模糊半径和相机距离挂钩,远处自动降低半径,让景深效果去接管。
和 TAA 打架。TAA 会利用历史帧信息,而我们的采样点如果每帧固定不变,TAA 会把这些噪点当作真实细节保留下来,形成“稳定的噪点”——看起来像画面脏了。反而如果每帧随机抖动采样点,TAA 反而能帮忙把噪点平均掉。所以如果你的项目开了 TAA,可以在采样偏移上加一点基于帧序号的抖动(View.GameTime或者用DitherTemporalAA节点),效果会好很多。
和屏幕空间反射打架。毛玻璃本身如果是光滑表面,会反射环境,而 SSR 在半透明物体上的支持有限。最明显的表现是反射区域出现“缺块”。处理方式是给玻璃一个不依赖 SSR 的反射来源,比如 Reflection Capture,或者在材质里关掉 SSR 贡献。
这些交叉问题没法一次性全解决,我的习惯是在材质里预留几个开关参数(bEnableDepthFade、bEnableFresnelMask、bUseProceduralNoise),做成静态开关,方便在不同场景里按需裁剪。
5.3 我个人踩过的三个坑
第一个是过度设计。我最早那版做了五层叠加:法线扰动、两层不同半径的模糊、色散、添加高光。跑起来确实好看,但开销顶得上半个后处理,而且参数之间互相影响,调一个崩三个。后来砍到两层,效果反而更干净。这行的经验是:毛玻璃的核心就是模糊 + 扰动,其他都是锦上添花,先保证核心稳。
第二个是忽略了材质实例的继承链。母材质里我写了NormalStrength = 0.002,但实例里有几个覆盖成了 0.02。项目里跑了一个月才被人发现某几块玻璃的磨砂特别夸张,一查是美术在复制粘贴实例的时候连参数一起复制了。现在我的习惯是把关键参数打上“不可覆盖”或者干脆用静态参数,从源头堵住。
第三个是对比度问题。我发现如果只做模糊不做任何调制,毛玻璃会让背后的场景整体变灰、对比度下降,看起来像隔着脏玻璃。解决方法是给模糊结果乘一个轻微的色调(Lerp到玻璃自身颜色),并且把亮度稍微提一点,视觉上会更接近真实玻璃的通透感。这个调整很细微,但去掉之后马上能感觉到“不对”。
最后分享一个我一直在用的小技巧:在编辑器里放一个 Debug 开关,把模糊结果和原始 SceneColor 做 A/B 比对。按一下键切换,能立刻看出模糊是不是真的起作用了,还是只是被色调变化骗了眼睛。这个调试习惯帮我省下的时间,比任何优化手段都多。