Yuyeyyy · Graphics

UE5 / Material

UE5 清漆与半透雨衣材质:从 UV1 接缝根因到透明物体描边

返回文章

项目里做武器和角色装备材质时,集中在三个问题上:清漆质感怎么做得对、半透雨衣怎么不出排序问题、透明物体怎么描边。这三个问题看着不相关,实际串起来是一条线——都是"材质特性与引擎渲染管线的假设不匹配"。这篇按这条线记一下,包括那个查了挺久的 UV1 接缝问题。

一、清漆:UE 的 Clear Coat 是双法线模型

先搞清楚模型

很多人把清漆理解成"在普通 PBR 上再加一层高光",在 UE 里这么理解会做错。UE 的 Clear Coat shading model 是两层独立 lobe:

Lo=(1−Fc)⋅fbase(nbottom)+fcoat(ncoat)L_o = (1 - F_c) \cdot f_{base}(n_{bottom}) + f_{coat}(n_{coat})

关键点:

项 说明
nbottomn_{bottom} 底层法线,走常规 PBR
ncoatn_{coat} 清漆层法线,独立输入,通常更平滑
FcF_c 清漆层的菲涅尔项,约 0.04 起,随视角上升
(1−Fc)(1 - F_c) 能量守恒的关键:底层高光被清漆反射掉的部分要扣掉

最后这条最容易漏。不乘 (1−Fc)(1-F_c) 的结果就是:清漆越亮,物体整体越亮,能量凭空多出来,在强光下会明显"过曝"。

参数

参数 作用 经验值
ClearCoat 清漆强度 0–1,武器烤漆一般 0.6–1.0
ClearCoatRoughness 清漆粗糙度 新漆 0.03–0.08,磨损处上调
底层 Roughness 底漆粗糙度 0.3–0.6

两层法线分开给是清漆质感的来源:底层给细节法线(划痕、磨损),清漆层给一张很弱的大尺度起伏(或者直接用默认法线)。如果两层用同一张法线,清漆就退化成了"加了个高光",没有那种"漆面浮在图案之上"的层次。

ORN 通道打包

项目里用的是 ORN 打包(Occlusion / Roughness / Normal),法线 XY 打包在 B / A 通道:

通道 内容
R Ambient Occlusion
G Roughness
B Normal.X
A Normal.Y

两个必须注意的设置:

  1. 采样类型设 LinearColor(sRGB 关闭)。法线的 XY 是 -1..1 的有符号数据,走 sRGB 转换会被 gamma 掰弯,法线直接废掉。
  2. 解包后要 Normalize。Z 是按 ∥n∥=1\|n\| = 1 反解出来的,不归一化就等于把误差带进后续所有计算——这点看着基础,但正是下面那个接缝 bug 的一半根因。
// 解包:B/A → 法线
float2 nxy = Tex.rg * 2.0 - 1.0;        // 假设已取到 B/A 通道
float nz = sqrt(saturate(1.0 - dot(nxy, nxy)));
float3 N = normalize(float3(nxy, nz));  // <-- 这行不能省

二、UV1 接缝:一个查了两层原因的问题

现象

武器模型在 UV1 接缝处有一条明显的光照断线。不是贴图没对齐,是明暗本身不连续——同一条边,两侧的高光位置对不上。第一反应是"UV 展开问题",但检查 UV1 是连续的。

第一层原因:解包函数末尾缺 Normalize

查材质函数 MF_UnPackNormal,末尾确实没有 Normalize。补上之后接缝变淡了,但没消失。

这说明:缺 Normalize 是放大器,不是根因。它让法线误差累积,但不产生接缝本身。

第二层原因:硬边导致的切线空间断裂

继续往下挖才找到真正的根因:模型在接缝处是硬边(split normal / 拆点)。

逻辑链是这样:

硬边 → 同一个位置拆成两个顶点,各自带不同法线
     → 两侧的切线空间(TBN)基准不同
     → 同一张切线空间法线贴图,在两侧解出的世界法线不一样
     → 高光位置突变 → 肉眼看到的"接缝"

这是几何层面的固有矛盾,不是材质参数能修好的。硬边是模型拓扑的有意选择(保住硬朗的转折),而切线空间法线贴图要求连续的切线空间——两者在硬边处天然冲突。

能缓解到什么程度

按"能做 / 不能做"分开:

手段 效果 代价
解包后 Normalize 消除误差累积,接缝变淡 几乎为 0,必做
接缝处切线空间平滑 缓解明显 需要在建模侧处理
用 MikkTSpace 统一切线算法 保证 DCC 与引擎一致 需统一烘焙/导入流程
完全消除 做不到 硬边 + 切线空间法线是固有冲突

最后一条要跟美术说清楚:UV 接缝 + 硬边 = 固有接缝,只能缓解,无法消除。这句话不是推卸责任,是物理事实。如果接缝位置很显眼,正确的解法是把接缝挪到不显眼的地方,而不是继续调材质参数。

5 秒验证:临时把材质切到 World Space Normal 显示(或者直接用无贴图的纯色 + 单一光源),如果接缝还在,就是几何/切线问题;如果消失,才是贴图问题。

三、半透雨衣:别直接上 Translucent

雨衣要的是"半透但看得见轮廓"的塑料/橡胶感。直接用 Translucent 会有三个老问题:

问题 原因
排序错误 半透明按物体中心排序,自身穿插时必然出错
不写深度 后续效果(DOF、描边)拿不到深度
overdraw 层数一多,移动端带宽直接爆

项目里的取舍是用 Dithered Opacity(抖动透明)替代真半透:

// Dithered Opacity:用屏幕空间抖动图案做二值化
// 配合 TAA 时,抖动在时间上被平均掉,看起来就是半透
float dither = DitherPattern(ScreenPosition.xy);   // 如 Bayer 4x4 / Interleaved Gradient
clip(Opacity - dither);
方案 排序 深度 成本 适用
Translucent 容易错 不写 高(overdraw) 必须真混合的场合
Masked + Dither 无问题 写 低 雨衣这类薄壳,首选
Thin Translucent 一般 不写 中 需要真实折射时

代价是需要 TAA 才能把抖动"抹平",关掉 TAA 会看到明显的网点。项目有 TAA,所以这笔交易划算。

四、透明物体的高亮描边

透明物体描边难在一点:描边方案几乎都依赖深度,而透明物体不写深度。

方案对比

方案 原理 透明物体可用 成本
背面外扩(Backface Outline) 第二遍画放大的背面 可用 一次额外 draw
屏幕空间深度差 相邻像素深度突变处描边 不可用(无深度) —
Fresnel Rim 视角掠射处提亮 可用 几乎为 0
CustomDepth + 后处理 单独渲一遍深度再描 可用 一次额外 pass

落地:Fresnel Rim 为主

对半透雨衣,最后用的是 Fresnel——因为它便宜,而且"边缘发光"本身就是想要的效果:

// Fresnel 描边
float3 V = normalize(CameraVector);
float fres = pow(1.0 - saturate(dot(N, V)), FresnelPower);

// 只在需要时高亮:Highlight 0..1 由游戏逻辑驱动
float3 outline = OutlineColor * fres * Highlight * OutlineIntensity;

// 加到自发光上,不要加到漫反射
EmissiveColor += outline;

要点:

  • 加在 Emissive 而不是 BaseColor/Diffuse——描边是"发光",混进漫反射会被光照二次调制,暗处就看不见了。
  • FresnelPower 控制描边粗细,3–5 比较自然;再大就只剩一圈硬边。
  • Highlight 做成参数,游戏逻辑(选中、受击、可拾取)直接驱动,不用切材质。

需要精确描边时

如果需求是"精确的外轮廓线"(不是发光感),Fresnel 就不够用了,改用 CustomDepth + 后处理:

  1. 物体勾 Render CustomDepth Pass,单独渲一遍深度
  2. 后处理里对 CustomDepth 做边缘检测
  3. 描边结果叠加回画面

好处是描边不受材质透明与否影响,坏处是多一个 pass,且描边是屏幕空间的,粗细会随距离变化。

性能预算

这套材质在移动端/主机上都跑过,几个值得记的数:

项 预算参考
清漆材质指令数 比默认 Lit 多约 15–25%,双法线是主要增量
半透明层数 控制在 3 层以内,超过就考虑 dithered
描边 Fresnel 方案几乎免费;CustomDepth 方案多一个 pass
纹理采样 ORN 打包后 3 张变 1 张,采样数省 2

清漆的开销主要来自双法线——每个像素算两遍 lobe。如果项目里清漆材质铺得很开,值得考虑在低 LOD 上降级成普通 PBR(LOD 切换材质)。

小结

这三个问题串起来其实是同一件事:引擎的渲染管线有一套默认假设(深度是连续的、法线是归一化的、切线空间是连续的),一旦你的资产或材质违反其中某一条,就会在看起来完全不相干的地方出问题。

所以排查这类 bug 的顺序我固定是:

  1. 先几何后材质——顶点焊接、UV 接缝、硬边切线空间,这些没排查干净之前不要动材质参数
  2. 确认数据本身对不对(通道、sRGB、归一化)
  3. 最后才是调参数

第 2 步和第 3 步最容易搞反。参数能调出"看起来还行"的效果,但根因还在,换个光照条件就原形毕露。

评论