UE5.8 PBR 贴图工作流程:金属粗糙度管线的踩坑实录

📑 目录

UE5.8 PBR 贴图工作流程:金属粗糙度管线的踩坑实录

引子:一张贴图毁掉一个角色

场景美术小李把第一个角色导入引擎后,盯着视口发了十分钟呆。角色盔甲在预览光照下泛着奇怪的塑料感,皮肤像涂了一层蜡,头发边缘还泛着青色光晕。他回头检查源文件:Substance Painter 里的预览明明很正常,导出设置也是默认的 PBR Metal Roughness。问题出在哪?

TA 阿伟走过来,先看了他的贴图命名,又点开几张 Texture 的 Details 面板,叹了口气:「你的 BaseColor 被当成 Linear 导入了,Roughness 反而勾了 sRGB,法线贴图还用的是 OpenGL 方向。三个设置全反了,渲染能对才怪。」

这就是 PBR 贴图工作流的日常。PBR 本身并不难理解,但它把正确性从美术直觉转移到了数据和流程上。一张贴图用错色彩空间,一个通道上下颠倒,都会让最终画面偏离预期。本文从 UE5.8 的实际项目出发,把金属/粗糙度工作流拆成八个环节讲清楚。

一、PBR 到底在模拟什么

PBR 是 Physically Based Rendering 的缩写,意思是基于物理的渲染。它的核心目标在于保证材质参数具备物理一致性,这样画面在不同光照下都能稳定。在传统手绘贴图时代,同一个石头在清晨和正午的光照下需要两张不同的贴图;PBR 则希望同一组 BaseColor、Roughness、Normal 在不同光照下都能给出合理结果。

这种一致性来自两个基本假设。能量守恒要求表面反射出去的光子能量不可能超过入射光。微表面模型则把任何光滑或粗糙的表面看成无数微小镜面法线朝向不同的高光点集合。渲染时,Shader 根据视角和光照方向统计这些微表面的反射贡献。

微表面 BRDF 通常写成三项乘积:

fr(ωi,ωo)=D(h)F(ωo,h)G(ωi,ωo,h)4(nωi)(nωo)f_r(\omega_i, \omega_o) = \frac{D(h) \cdot F(\omega_o, h) \cdot G(\omega_i, \omega_o, h)}{4(n \cdot \omega_i)(n \cdot \omega_o)}

其中 DD 是法线分布函数,描述微表面法线的集中程度;FF 是菲涅尔项,决定 grazing angle 处的反射增强;GG 是几何遮蔽项,处理微表面之间的互相遮挡。Roughness 越接近 0,DD 越集中在宏观法线周围,高光就越锐利;Roughness 越接近 1,微表面法线越分散,反射越接近漫射。

UE5.8 默认使用金属/粗糙度工作流。在这个工作流里,所有非金属的 F0F_0 被固定为 0.04,金属的 F0F_0 由 BaseColor 直接提供。这样做的好处是参数少、不易出错;代价是金属和非金属的过渡区必须由 Metallic 贴图精确控制。

二、六张贴图的物理含义

一套标准的金属/粗糙度贴图通常包含六张图。每张图负责一个独立的物理量,混用或误删都会出问题。

2.1 BaseColor

BaseColor 也叫 Albedo,表示材质在完全漫反射条件下的固有色。它不应该包含任何光照信息:没有阴影、没有高光、没有 AO 暗角。理想情况下,纯非金属的 BaseColor 值应该在 sRGB 0.02 到 0.08 之间,即线性值 0.017 到 0.065;金属的 BaseColor 可以更高,因为金属没有漫反射,其颜色来自 tinted 反射。

小李的盔甲 BaseColor 里偷偷画了一些白色高光笔触,这在 PBR 工作流里是大忌。那些白色会被 Shader 当成漫反射颜色,在任意光照下都发亮,角色就会像塑料。很多美术在画 BaseColor 时容易把值推得太高。判断标准是:把贴图导入引擎后,在 BaseColor 视口里观察,非金属区域不应该有接近纯白的像素。如果出现大片亮部,通常意味着贴图里残留了高光或反射信息。

2.2 Metallic

Metallic 是一张灰度贴图,告诉 Shader 每个像素是金属还是非金属。值为 1 表示金属,使用 BaseColor 作为 F0F_0;值为 0 表示非金属,使用固定的 0.04 作为 F0F_0。中间值只在过渡区域使用,比如生锈边缘或灰尘覆盖。

实践中,Metallic 应该是二值化的,除了过渡 mask 外尽量不要出现大量 0.5。长期经验表明,把金属和非金属的边界画清楚,比追求柔和的过渡更耐看。

2.3 Roughness

Roughness 控制微表面的光滑程度。0 表示镜面级光滑,1 表示完全粗糙。Roughness 对光照方向非常敏感,因此必须保证值域准确:全图平均值通常落在 0.1 到 0.9 之间,极少出现纯黑或纯白。

角色头发的高光宽度、皮革的毛孔反射、金属的拉丝方向,都靠 Roughness 表达。Roughness 贴图里如果有一块噪点,渲染时就会像表面撒了沙子。Roughness 贴图还有一个常见误区:以为越光滑越真实。其实现实世界里几乎没有 Roughness 为 0 的表面。即使是镜子,Roughness 也往往在 0.02 到 0.05 之间。把金属盔甲的 Roughness 压到 0 会让高光变成刺眼的亮点,失去材质的体积感。

2.4 Normal

Normal 贴图存储的是每个 texel 对应的局部表面法线方向,用来在不增加多边形的情况下表现凹凸。它本身不直接参与颜色计算,而是改变 BRDF 里的 nn 向量。法线贴图最常见的格式是 RGB 三通道,分别对应切线空间下的 X、Y、Z 分量。

法线贴图对精度敏感。UE5.8 默认使用 BC5 压缩,只保留 X 和 Y 通道,Z 在 Shader 里重建。这种压缩比 BC1 或 BC3 更清晰,但会多占一张贴图的内存带宽。Normal 贴图只能改变表面法线方向,不能改变 silhouette。如果模型边缘本身是平的,法线贴图无法让边缘看起来有厚度。需要真正改变轮廓的地方,应该加面数或用置换贴图,而不是把法线强度拉满。

2.5 Ambient Occlusion

AO 表示环境光被周围几何体遮挡的程度。它不是阴影,而是描述表面凹陷处接收间接光照变少的现象。AO 通常只乘在间接漫反射项上,直接光照和镜面反射不应乘以 AO,否则高光会在凹陷处莫名其妙地消失。

UE5.8 的默认材质模板里,AO 默认连到 Ambient Occlusion 输入,不参与直接光计算。如果你把 AO 乘进 BaseColor,角色眼窝和腋下会黑得像煤球。烘焙 AO 时要注意采样半径。半径太小,只有极细的缝隙会变暗,整体缺乏体积感;半径太大,连原本应该受光的地方也会变黑。项目应该规定一个统一的烘焙半径,比如 0.5 米,并写在文档里。

2.6 Emissive

Emissive 表示自发光颜色。它不参与光照计算,直接以叠加方式输出到屏幕。霓虹灯、手机屏幕、能量核心都适合用它表现。Emissive 的强度通常用 HDR 值,配合 Bloom 后处理使用。

自发光没有物理上限,但滥用会导致画面过曝。建议为 Emissive 设置一个亮度上限,并在材质实例里用标量参数控制强度,方便后期统一调整。

flowchart TD
    subgraph dcc ["DCC 出图"]
        sbspainter["Substance Painter"]
        photoshop["Photoshop"]
        designer["Substance Designer"]
    end
    subgraph textures ["纹理资产"]
        bc["BaseColor sRGB"]
        mt["Metallic Linear"]
        rg["Roughness Linear"]
        nm["Normal Linear"]
        ao["AO Linear"]
        em["Emissive sRGB"]
    end
    subgraph import ["UE5.8 导入"]
        factory["Texture Factory"]
        settings["导入设置"]
    end
    subgraph material ["材质系统"]
        mat["Default Lit Material"]
        shader["Pixel Shader"]
    end
    sbspainter --> bc
    sbspainter --> mt
    sbspainter --> rg
    sbspainter --> nm
    sbspainter --> ao
    sbspainter --> em
    bc --> factory
    mt --> factory
    rg --> factory
    nm --> factory
    ao --> factory
    em --> factory
    factory --> settings
    settings --> mat
    mat --> shader

这张图说明了六张贴图在 DCC 工具里生成,经过导入设置,最终进入 Shader 计算的完整过程。每一张贴图在进入 Texture Factory 时都要做一次色彩空间和压缩格式的判定,这一步决定了后续像素在 Shader 里的含义。

三、色彩空间:sRGB 与 Linear 不能乱

贴图不是颜色,是数字。同一个 RGB 值,在不同色彩空间下代表的物理光强完全不同。sRGB 是为了适配人眼对暗部更敏感的特性而做的非线性编码,近似满足:

ClinearCsRGB2.2C_{linear} \approx C_{sRGB}^{2.2}

Linear 则是与物理光强成正比的数据。渲染管线必须在计算前把所有输入转换到 Linear 空间,否则加法和乘法都会出错。

UE5.8 的 Texture 资产有一个 sRGB 复选框。勾上表示这张贴图在导入时会被解码为 Linear 再参与渲染。BaseColor 和 Emissive 通常勾 sRGB,因为它们在 DCC 工具里是以人眼感知的方式绘制的。Metallic、Roughness、Normal、AO 必须取消 sRGB,因为它们本身就是物理标量或向量,不需要 gamma 校正。

小李的错误就在这里。他把所有贴图都勾了 sRGB,结果 Roughness 0.5 在 Shader 里被当成 0.52.20.220.5^{2.2} \approx 0.22,表面变得异常光滑;BaseColor 没勾 sRGB 的反而被线性使用,颜色发暗发脏。

flowchart TD
    A["收到新贴图"] --> B{是否包含颜色信息}
    B -->|"是"| C["BaseColor Emissive"]
    C --> D["勾选 sRGB"]
    B -->|"否"| E["Metallic Roughness Normal AO"]
    E --> F["取消 sRGB"]
    D --> G["Shader 在 Linear 空间计算"]
    F --> G
    G --> H["正确光照结果"]

判断依据很简单:贴图是给人眼看的颜色,就勾 sRGB;贴图是给 Shader 算物理量的数据,就取消 sRGB。另一个容易忽略的细节是:即使勾了 sRGB,UE5.8 也只在采样时做解码,导出或读取像素值时看到的仍然是 sRGB 数据。因此不要在 Texture Editor 里用肉眼判断 Linear 转换是否正确,而应该在 Lit 视口里观察最终效果。

四、贴图导入设置

把贴图拖进 Content Browser 后,UE5.8 会弹出 Texture Import 对话框。对话框里有几个决定渲染质量的选项,必须逐项检查。

Compression Settings:BaseColor 用 Default(BC1/BC3 或 BC7);Normal 用 Normalmap(BC5);Mask 或单通道数据用 Grayscale(BC4)。如果 Roughness 单独成图,也可以保留 Default,但会浪费两个空通道。

Texture Group:决定贴图的 LOD Bias 和流送优先级。World 用于场景贴图,Character 用于角色,Weapon 用于武器。分组错误会导致远处贴图过早模糊。

Mip Gen Settings:控制 mipmap 生成算法。Sharpen 适合 Normal 和细节纹理;Blur 适合柔和的 BaseColor。默认的 SimpleAverage 在多数情况下够用。

Max Texture Size:限制贴图最大分辨率。移动端项目通常限制在 1024,PC 端可以开到 2048 或 4096。注意这个值只影响导入后的最大尺寸,原始文件可以更大。

Never Stream:勾选后贴图不会进入纹理流送池,会常驻显存。UI、特效贴图、小图标适合勾选;场景大贴图不要勾。

Defer Compression:开发阶段可以勾选,加快导入速度;打包前必须取消,否则 Cook 时会 uncompressed 数据爆炸。对于需要透明通道的贴图,比如 Decal 或植被叶片,Compression Settings 可以选 TC_Default 并保留 alpha,或者根据用途选 TC_EditorIcon 之类。alpha 通道在压缩时容易被 BC1 的 1bit alpha 截断,所以带半透明渐变的贴图应该优先考虑 BC7 或 BC2。

导入完成后,建议打开 Texture Editor 检查 Mip 0 的像素值是否正常。Normal 贴图应该呈现均匀的蓝紫色,BaseColor 不应有纯黑或纯白的死区。

sequenceDiagram
    participant A as "美术"
    participant B as "Content Browser"
    participant C as "Texture Factory"
    participant D as "Texture Asset"
    participant E as Material
    A->>B: 拖拽贴图文件
    B->>C: 弹出导入设置
    A->>C: 配置 sRGB Compression Group Mip
    C->>D: 生成 uasset
    A->>D: 在 Texture Editor 检查
    D->>E: 赋给材质采样器
    E->>A: 在视口验证效果

五、Normal 贴图:OpenGL 与 DirectX 之争

法线贴图在 Y 轴方向上分为两种约定。OpenGL 约定 Y 向上,DirectX 约定 Y 向下。UE5.8 的渲染 API 是 DirectX 12,因此默认使用 DirectX 法线。如果你的法线贴图是在 Blender 或某些 Web 工具里生成的,很可能是 OpenGL 方向。

方向反了的法线贴图会在凹陷处表现为凸起,凸起处表现为凹陷,角色头发和锁子甲尤其明显。UE5.8 提供了两种修正方式:一是在导入时勾选 Flip Green Channel,二是在材质里用 OneMinus 节点反转 G 通道。

判断当前法线方向的土办法:找一张已知的凹凸贴图,导入后观察高光走向。如果砖缝看起来像凸起,说明绿通道反了。

阿伟给小李的规范是:所有从 Substance Painter 导出的法线直接导入 UE5.8,因为 Painter 的 UE5 预设已经按 DirectX 输出;从 Blender bake 的法线必须勾 Flip Green Channel;从外部购买的素材包先看预览图,方向不对立刻改,不要留到材质阶段再补丁。

六、贴图通道打包

通道打包是把多张灰度贴图塞进一张 RGBA 贴图的不同通道,减少采样次数和显存占用。UE5.8 项目里最常用的打包方案是 ORM:

  • R 通道:Ambient Occlusion
  • G 通道:Roughness
  • B 通道:Metallic

也有项目把 AO 单独放一张,把 Roughness、Metallic、Specular 或自定义 mask 打包。通道打包没有全球统一标准,关键是团队内部约定一致,并在材质里用 Component Mask 正确取出。

下面是一个用 Python 和 Pillow 把三张灰度图合成 ORM 贴图的示例:

from PIL import Image

def pack_orm(ao_path, roughness_path, metallic_path, out_path):
    ao = Image.open(ao_path).convert('L')
    roughness = Image.open(roughness_path).convert('L')
    metallic = Image.open(metallic_path).convert('L')

    # 确保尺寸一致
    size = ao.size
    roughness = roughness.resize(size)
    metallic = metallic.resize(size)

    orm = Image.merge('RGB', (ao, roughness, metallic))
    orm.save(out_path)

pack_orm('T_Armor_AO.png', 'T_Armor_Roughness.png', 'T_Armor_Metallic.png', 'T_Armor_ORM.png')

导入 UE5.8 后,这张 ORM 贴图要取消 sRGB,Compression Settings 选 Masks。在材质里用 Mask 节点分别取 R、G、B 通道,再连到对应的输入。

通道打包有个细节:每张灰度图在 DCC 里可能以不同位深保存,合并前必须统一。PNG 是 8bit,TGA 可以 16bit,EXR 是 16bit 或 32bit 浮点。项目通常规定通道打包源文件全部使用 16bit PNG,避免在 R 和 G 通道之间出现台阶。

graph LR
    AO["Ambient Occlusion"] -->|R| ORM["ORM Texture"]
    Roughness -->|G| ORM
    Metallic -->|B| ORM
    ORM -->|R| MatAO["Material AO"]
    ORM -->|G| MatRough["Material Roughness"]
    ORM -->|B| MatMetal["Material Metallic"]

七、工作流规范

PBR 贴图工作流不是技术问题,是协作问题。美术、TA、程序和外包必须对命名、目录、色彩空间和打包方案达成一致,否则项目后期会陷入找贴图、改贴图、重新烘焙的循环。

阿伟团队使用的规范如下:

命名规则T_<AssetName>_<Type>_<Variant>。例如 T_Armor_Chest_BaseColorT_Armor_Chest_ORMT_Armor_Chest_Normal。T 前缀表示 Texture,后面紧跟资产名、部位、类型和变体。

目录结构:按资产类型分文件夹。/Game/Textures/Characters//Game/Textures/Environment//Game/Textures/Weapons/。共享贴图放在 /Game/Textures/Shared/,比如噪点、细节法线、渐变图。

导出模板:Substance Painter 里为 UE5.8 建立专用导出预设,一次性输出 BaseColor、Normal、ORM、Emissive,并自动按命名规则写入文件名。美术不需要每次手动选格式。

版本控制:贴图源文件和 uasset 一起提交。PNG/TGA 源文件放在 /SourceArt/Textures/,uasset 放在 /Game/Textures/。这样 DCC 修改后可以追踪历史,引擎资产也能被其他人直接加载。

检查清单:每次提交贴图前,美术自检四项:sRGB 是否勾选正确、Compression Settings 是否匹配类型、法线方向是否 DirectX、Max Texture Size 是否符合项目预算。

八、常见错误

错误一:BaseColor 里画阴影。PBR 的阴影应该来自光照和 AO,而不是贴图。BaseColor 越干净,换光照环境后越稳定。

错误二:Metallic 贴图出现 0.5。除了泥土、锈迹等过渡区域,金属和非金属的边界应该尽量分明。0.5 会让 Shader 使用 0.04+0.5(Fmetal0.04)0.04 + 0.5 \cdot (F_{metal} - 0.04)F0F_0,结果往往是油腻的灰色。

错误三:Roughness 值超出 0-1。UE5.8 默认会把 Roughness clamp 到 0-1,但超出范围的值在 mipmap 下可能产生奇怪的平均结果。绘制 Roughness 时只使用合法灰阶。

错误四:Normal 贴图勾了 sRGB。法线贴图是向量数据,勾 sRGB 会导致 gamma 解码,所有凹陷和凸起都变形。导入后第一件事就是取消 sRGB,并检查 Compression Settings 是否为 Normalmap。

错误五:AO 乘进直接光照。AO 只影响间接光,乘进 BaseColor 或 Specular 会让直接光高光消失,角色眼窝和墙角会死黑。

错误六:通道打包后忘记取消 sRGB。ORM 是物理数据,勾 sRGB 后 Roughness 和 Metallic 都会被 gamma 扭曲,盔甲看起来要么像镜子要么像泥。

错误七:贴图分辨率不统一。一张 4K BaseColor 配一张 1K Roughness,材质采样时会出现像素错位,边缘处颜色和粗糙度不匹配。项目应该规定同一资产的贴图分辨率一致。

错误八:依赖默认导入设置。UE5.8 的默认设置对通用素材没问题,但对项目定制的工作流往往不够。每个项目应该有自己的 Import Preset,并在导入对话框里默认加载。

错误九:把 Roughness 和 Metallic 画成完全相同的变化。现实中金属磨损区域的 Roughness 会升高,但非金属区域的 Metallic 不应该随 Roughness 变化。两张贴图应该分别表达不同的物理现象,不要复制粘贴。

错误十:忽略平台差异。移动端 GPU 对贴图压缩格式有严格要求,比如 ASTC 是主流;PC 端可以用 BC 系列。跨平台项目应该在导出模板里设置多套预设,而不是手动逐张改。


小李后来按这套流程重新整理了一遍角色贴图。BaseColor 去掉了手绘高光,Roughness 按磨损区域重新绘制,Normal 绿通道翻转,ORM 单独打包。导入后盔甲的金属感恢复了,皮肤不再蜡黄,头发边缘的青光也消失了。他截图发给阿伟,阿伟只回了一句:「这就对了,PBR 不看手活,看流程。」

PBR 不会让画面自动更惊艳,它是一套保证画面在不同光照、不同机器、不同人手里都稳定一致的方法。贴图工作流里的每一个勾选项、每一个命名、每一个通道,都是在为这种一致性投票。