第08章 · 材质系统:PBR 直觉与材质函数

📑 目录

导语:评审会上,美术总监指着屏幕说地面不够湿。程序把 Roughness 从 0.7 一路改到 0.3,画面纹丝不动——查到最后,贴图红色通道里嵌着一个 0.9 的常量,参数被乘法项吃得干干净净。同一个月另一起事故:某场景 GPU 超时 4 毫秒,凶手是一块「只有二十个节点」的地形材质,它默默采样了十四张 4K 贴图。两个事故同一个病根:太多人把材质编辑器当调色盘用,而它其实是个编译器。

本章地图

mindmap
  root((第 8 章
材质系统)) PBR 直觉 四参数极端值对照 GGX 波瓣形状 数据流程序 节点数 ≠ 成本 采样才是大头 变体与 cook 时间 组织复杂度 材质函数 = 函数库 实例 = 类与对象 MPC = 全局广播 纹理预算 分辨率按屏幕占比 ORM 通道打包 排查决策树 效果与色彩 湿 / 布 / 皮拆解 低饱和 + 单点强调色

材质这一章分四步走:先给四个参数建立物理直觉,再搞懂这张节点图编译成了什么,然后学会用函数库和实例组织复杂度,最后落到「回响峡谷」的调色板纪律上。

8.1 PBR 四参数:别背定义,用极端值对照法

PBR(Physically Based Rendering,基于物理的渲染)的核心约定只有一条:材质不回答「这个像素是什么颜色」,它回答「光打到这个表面会怎么弹」。最终颜色是光照条件与材质参数共同算出来的——同一块砖在篝火边和阴雨天看起来不一样,这恰好是物理正确性的证明。

UE 默认光照模型下,真正决定表面性格的是四个输入:BaseColor、Roughness、Metallic、Normal。文档里的定义谁都会背,直觉背不出来。建立直觉最快的办法是极端值对照法:把每个参数单独打到 0 和 1,看画面发生什么。中间值没有信息量,极端值才有。

同一束光打到四种表面:波瓣形状由 Roughness 决定,高光颜色由 Metallic 决定Roughness 0 · Metallic 0针尖白高光 + 漫反射抛光塑料 · 漆面 · 水洼Roughness 1 · Metallic 0高光摊成一片 + 强漫反射粉笔 · 哑光混凝土Roughness 0 · Metallic 1针尖有色高光 · 无漫反射抛光铜器 · 镜面金属Roughness 1 · Metallic 1宽的有色高光 · 无漫反射拉丝铝 · 磨砂金属入射光无色高光(非金属)着色高光(金属,取自 BaseColor)漫反射

对照着图,把四个极端组合记成一张表:

参数打到 0 的样子打到 1 的样子物理含义
Roughness镜面:高光缩成针尖大的亮点,环境反射清晰粉笔:高光摊开到近乎消失,只剩均匀漫反射表面微观起伏的尺度
Metallic电介质:漫反射为主体,高光永远是光源颜色,强度约 4%(F0≈0.04)金属:漫反射消失,全部颜色搬进高光,高光被 BaseColor 染色材质内部有没有自由电子

金属为什么没有漫反射:可见光进入非金属内部,经多次散射后再射出,形成漫反射;金属的自由电子在表层就把光要么反射要么吸收,「进入内部再出来」这条路根本不存在。所以金属的颜色只能住在高光里——金子发黄是因为它的反射光谱偏黄,不是因为它「表面是黄的」。

这里藏着新手第一个大坑:Metallic 的中间值在自然界基本不存在。把滑杆调到 0.3、0.7,出来的是「发灰的塑料铁」,既不是金属也不是非金属。工程上的正确形态是遮罩图:一张由 0 与 1 构成的区域划分——铁钉区域是 1,木柄区域是 0。Metallic 滑杆存在的意义是整张贴图统一覆盖,别拿它找中间态。

Roughness 的中间值倒是合法的(磨砂金属大约 0.3–0.5,皮肤 0.4–0.6),但也别凭感觉拧。查真实材料的参考表比眼睛靠谱:UE 官方文档 Physically Based Materials 词条附了常用材质的反照率与粗糙度范围,做新材质前先对一遍。

BaseColor 的纪律有两条。第一,它存的是反照率(albedo),不含光照:别把阴影、高光、AO 烘焙进去,那是 2010 年前手绘贴图时代的做法,进了 PBR 管线会被光照系统再算一次,画面发闷。第二,别用纯白纯黑:真实非金属的反照率大致落在 sRGB 50–240 的区间,超出范围的输入会让能量守恒失效——画面里出现「自己会发光的白墙」或者「吞光的黑洞」,多半是这条犯了。

Normal 是四个参数里最容易被神化的。它只修改每个像素的法线方向,不改轮廓,不产生自阴影。远看岩石立体,近看剪影还是光溜溜的多边形。它是光的假肢:好用,但别指望它替代几何细节——几何层面的方案是 Nanite(见第 7 章)。

四参数之外还有两个常用输入,顺带立好规矩。Specular 输入默认 0.5,对应 F0≈0.04,覆盖了自然界 99% 的非金属——平时别动它,乱调出来的「白玉质感」十有八九是物理错误。AO(Ambient Occlusion,环境光遮蔽)输入接缝隙遮蔽贴图,让砖缝、铆钉根部这些光照算不到的细小凹陷暗下去,是贴图细节感的来源;它和烘焙进 BaseColor 的阴影不同,只压间接光,不污染直接光照。

最后把四参数放进同一个物理框架收尾:能量守恒。一个表面反射出去的总能量不可能超过入射能量。Roughness 拉高不会让高光「消失」,是把同样的能量摊到更大的立体角上——波瓣变宽,峰值变暗。这笔账由 UE 默认的 GGX 高光分布模型替你管:

D(h)=α2π[(nh)2(α21)+1]2,α=R2D(\mathbf{h}) = \frac{\alpha^{2}}{\pi\left[(\mathbf{n}\cdot\mathbf{h})^{2}(\alpha^{2}-1)+1\right]^{2}},\quad \alpha = R^{2}

不用推导,只要会读。把表面想象成无数面微观小镜子,只有朝向 h\mathbf{h}、正好把光反射进眼睛的那批镜子贡献高光,D(h)D(\mathbf{h}) 就是「朝向 h\mathbf{h} 的镜子占多大比例」。Roughness RR 以平方形式进入 α\alphaRR 小的时候,分母对角度极其敏感,h\mathbf{h} 稍一偏离法线,DD 就暴跌,波瓣窄而高,高光就是针尖;RR 大的时候分母迟钝,波瓣矮而宽,高光摊成一片。平方是迪士尼 BRDF 定下的惯例:人眼对光滑段的差异最敏感,平方让 0–0.5 这段的控制更细腻。记住那个反直觉的点就够了——Roughness 改变的是波瓣的形状,高光的强弱只是伴生错觉

8.2 材质图是数据流程序,但节点数不等于成本

写过 shader 的人进材质编辑器会有种错觉:这也太轻松了,连连线就完事。这个类比本身是对的——材质图就是可视化的 fragment shader:每个节点是纯函数,连线是数据依赖,最左边那个输出节点是 return 语句,编译产物是 HLSL。凡是程序里成立的成本直觉,这里都成立;凡是可视化编程的坑,这里一个不少。

先看这张图会编译成什么:

flowchart TD
    A["材质编辑器: 节点连线"] --> B[保存 / Apply]
    B --> C[节点图翻译为 HLSL
材质函数在此内联展开] C --> D[按用途展开 shader 变体
BasePass / 阴影深度 / 移动平台] D --> E[逐平台编译着色器] E --> F[写入 DDC 派生数据缓存
全团队共享编译结果] F --> G["运行时: 材质实例只改常量缓冲
不触发重编译"] H[改公共材质函数] -. 触发所有引用材质重编译 .-> C

一个材质按用途展开成一族 shader 变体:BasePass、阴影深度、静态光照路径、移动平台……变体数随特性组合膨胀。这意味着两件事:打包时 shader 编译时间约等于「材质数 × 变体数」;材质函数改一行,所有引用它的材质全量重编译——和第 4 章改公共头文件触发全工程重编译是同一个灾难模型,DDC(派生数据缓存)是全团队的救命稻草。

体感数字给个量级:一个引用了三四个函数、带两个静态开关的地形材质,改完点 Apply,单机等上几十秒到几分钟是常态——真正在编译的是它身后几百个变体,这一个材质只是导火索。大团队里「改材质五分钟,编译半小时」不是段子,这就是为什么美术迭代材质时喜欢先关掉用不到的特性开关,也是为什么材质函数库的改动要攒批提交。

再说三个材质编辑器特有的坑,每一个我都见人栽过。

坑一:节点数不等于成本。 一个 Add 是一条 ALU 指令,几乎免费;一个 Texture Sample 是一次显存读取,命中缓存也要几百个周期,未命中上千。二十个节点采样十四张 4K 贴图的材质,比两百个节点纯做数学运算的材质贵一个数量级。评估材质成本的顺序:先看纹理采样次数(材质编辑器的统计面板能查),再看指令数(Instructions),最后才轮到节点总数——节点总数基本不代表任何东西。导语里那个 GPU 事故,排查报告上写的就是这一行。

坑二:同一贴图采样两次,就是两次的钱。 两个 Texture Sample 节点引用同一张贴图,生成的是两条独立采样指令。正确姿势是一个采样节点,输出连到所有用它的分支。反过来,用不同 UV 采样同一贴图没法合并,那是货真价实的两次采样,该付就付。

坑三:If 和 Static Switch 的代价不在同一个账本上。 GPU 是 SIMT 执行,动态分支两边可能都算一遍,材质里优先用 Lerp、Step 这类无分支数学替代 If 节点。Static Switch(静态开关)是编译期分支,运行时零成本,但每多一个开关,变体数翻倍——它拿 cook 时间换运行时,适合「画质档位」这类配置项,不适合逐帧变化的逻辑。

还有一个直觉要植入:材质是逐像素执行的。4K/60fps 下,一个占满屏幕的材质每秒执行约五亿次。任何「反正不贵」的节点,先乘上这个数再说话。能在蓝图里算一次、经参数传进来的东西——比如全局风向、累积时间——别放在像素里算五亿次。

8.3 材质函数与实例:把复杂度关进笼子

项目的材质复杂度没法消灭,只能分层关押。UE 给了三层笼子:函数、实例、参数集。

MaterialFunction 是材质界的函数库。 把「湿度混合」「世界空间苔藓」「三平面映射」这类通用节点组封装成函数,几十上百个材质共享。它在编译期内联展开,运行时零开销——成本全部发生在编译机上。麻烦也在这:函数改一行,所有引用它的材质重编译;嵌套到第四层的函数库,改一次全项目 cook 机排队半小时。团队规则两条:函数嵌套不超过两层;叶子函数进「冻结名单」,动它要像动公共头文件一样走评审。

MaterialInstanceConstant(MIC)是材质的实例化机制。 父材质定结构、暴露参数,实例只填数值,共享父材质的编译产物,自己只多一张参数覆盖表。类比面向对象:父材质是类,实例是对象。美术要一百种颜色的墙?一个父材质加一百个实例,而不是一百个材质——后者的 shader 编译量和维护成本都是灾难。参数化程度是父材质设计的第一决策:暴露太少,实例干不了活;暴露太多,面板像飞机驾驶舱,美术不敢碰。经验值是 5 到 15 个参数,超出就该拆材质了。

MaterialInstanceDynamic(MID)是运行时改参数的正道。 受击闪白、拾取高亮、技能充能条——所有「游戏里会动的材质表现」都走 MID。下面这段解决「玩家瞄准收集品时高亮」的问题:

// 拾取物被瞄准时高亮: 改参数, 不换 shader, 不重编译
void ACollectibleItem::SetHighlighted(bool bHighlighted)
{
    if (!HighlightMID)
    {
        UMeshComponent* Mesh = FindComponentByClass<UMeshComponent>();
        // 首次调用时包装索引 0 处的材质, 返回 MID
        HighlightMID = Mesh->CreateDynamicMaterialInstance(0);
    }
    // 父材质里暴露的 Scalar 参数, 实例只写常量缓冲
    HighlightMID->SetScalarParameterValue(
        FName("EmissiveBoost"), bHighlighted ? 6.0f : 0.0f);
    // 强调色统一从配置读, 保证全项目只有同一种橙
    HighlightMID->SetVectorParameterValue(
        FName("AccentColor"), AccentColor);
}

要点:MID 只能改父材质里暴露成参数的东西,没参数化的节点运行时是死的。所以写父材质时,「未来可能要动态调的东西」要全部提前参数化——这是写父材质和写普通材质的思维差异。反面教材也列一下:运行时动态 Spawn 一堆 MIC 资产、或者代码直接改 UMaterial,前者是资产管理灾难,后者会让所有实例一起变,排查时能气笑。

Material Parameter Collection(MPC)是全局广播。 一张全局参数表,所有材质都能读,改一个值,几百个材质同一帧响应:天气系统的 Wetness、昼夜的 SunTint、区域毒气浓度。它相当于材质世界的环境变量——方便,但乱用就是全局变量灾难。纪律只有一条:MPC 只放「整个场景共享」的量,单物体状态走 MID。混用的后果是「为什么全场景的箱子都在闪」这类灵异 bug。

Material Layers(材质层)是第四层笼子,也是最容易被滥用的一个。 思路类似 Photoshop 图层或装饰器模式:底层是岩石,往上叠苔藓层、积雪层、血渍层,每层独立编辑,运行时按遮罩混合。开放世界大团队爱用它——同一块地面的十几种变体不用做十几张材质,图层组合出来就行。代价是实打实的:每层都是一套完整采样,混合时全部付费,层数乘采样数上去之后,性能账比多张独立材质还难看;资产管线的学习成本也高。「回响峡谷」这种竖切规模用函数加实例就够了,材质层留给「远港 Online」的大世界(见第 16、17 章)再评估。一句话:层是组织手段,不是免费午餐,叠几层就付几层的钱。

六者的关系一张图收拢(Material Layers 在图上与函数同级,都是父材质的组成件):

classDiagram
    class UMaterial {
        <<资产>>
        节点图 + ShadingModel
        编译出一族 shader 变体
    }
    class UMaterialInstanceConstant {
        <<资产>>
        Parent 指针 + 参数覆盖表
        共享父材质编译产物
    }
    class UMaterialInstanceDynamic {
        <<运行时对象>>
        SetScalarParameterValue()
        只能改暴露的参数
    }
    class UMaterialFunction {
        <<资产>>
        可复用节点片段
        编译期内联展开
    }
    class UMaterialParameterCollection {
        <<资产>>
        全局标量 / 向量参数表
        一改全场景同帧生效
    }
    UMaterialInstanceConstant ..> UMaterial : Parent 指向
    UMaterialInstanceDynamic --|> UMaterialInstanceConstant : 继承
    UMaterial ..> UMaterialFunction : 内联调用
    UMaterial ..> UMaterialParameterCollection : 读取

8.4 纹理预算:采样次数才是材质的账单

上节说了纹理采样是大头,这节把账算细。单像素的采样成本大致是:采样次数 × 纹理内存占用 × 过滤模式开销,再乘上屏幕像素数。

先建立数字感觉。一张 4096×4096 的 RGBA8 贴图裸数据 64MB,带上完整 mipmap 链约 85MB;BC 压缩后降到四分之一(BC7,8bpp)到八分之一(BC1,4bpp)。一个场景堆三十张 4K,纹理池(Texture Pool)先爆,引擎随后强制降级 mipmap,整个画面糊成一片——新手常把这一幕当成引擎 bug 报上来,其实是预算超支。

分辨率分配的经验法则(1080p 到 4K 项目通用):主角皮肤与面部 4K,环境大件和地形 2K,手持道具 1K–2K,远景杂物 512–1K。分配依据是屏幕占比——重要度只是直觉,屏幕占比才是物理:一个特写镜头里占满屏幕的道具配 512 贴图,就是穿帮现场;一面玩家永远隔着三米看的墙配 4K,纯属烧钱。

三条省钱纪律。第一,mipmap 是免费的 LOD,UE 默认生成,让远处像素自动采小图;给非 UI 贴图勾 No Mipmaps 等于主动放弃显存带宽优化。第二,地形这类大角度斜视的材质开各向异性过滤(Anisotropic Filtering),成本中等,地面清晰度提升立竿见影。第三,通道打包:把 AO、Roughness、Metallic 三张灰度图塞进一张图的 R/G/B 通道——业界叫 ORM 贴图——一次采样拿三个参数,是性价比最高的材质优化,没有之一。法线贴图走专用的 BC5 压缩(只存 XY 两通道,Z 由长度重建),别用颜色贴图的压缩格式,否则法线方向会偏。

再大的贴图怎么办?虚拟纹理(Virtual Texture)把 8K 以上的贴图按页流送,大地形和超大背景用得多。代价是固定开销加一个反馈 pass,「回响峡谷」这种规模的竖切用不上,「远港 Online」的大世界再议(见第 16 章)。

预算落地靠流程,不靠自觉。两张表管全局:一是 Texture Group(纹理分组),把贴图按用途归组(角色、环境、特效、UI),每组统一尺寸上限和压缩设置,引擎按组批量降级时不会误伤主角的脸;二是命名规范,贴图名带尺寸和后缀——T_Rock_01_2K_BC7、T_Rock_01_ORM、T_Rock_01_N——让人在内容浏览器里一眼看出谁超标、谁是法线、谁被打过包。五十张贴图靠人肉记得住,五千张贴图只能靠规范。

材质出了问题去哪查?按这棵树走,比凭感觉翻节点快十倍:

flowchart TD
    A[某视角 GPU 超预算] --> B[切 Shader Complexity 视图 Alt+8]
    B --> C{大面积红色?}
    C -- 否 --> D[不是材质问题
去查几何与光照, 见第 6、7 章] C -- 是 --> E{红在透明物体上?} E -- 是 --> F[Overdraw 叠层计费
收缩粒子与雾片面积, Alt+9 复核] E -- 否 --> G[材质统计面板查采样数] G --> H{Texture Sample 超 8 次?} H -- 是 --> I[ORM 打包 / 砍次要细节层
合并重复采样] H -- 否 --> J[ProfileGPU 看指令数
砍多余 Power / 距离场 / 多层噪声] I --> K[核对纹理分辨率
环境件 ≤ 2K, mipmap 开启] J --> L[复查静态开关
变体数是否爆炸] K --> L

最后是取舍。同一个视觉效果常有三种实现,成本差一个数量级。这张象限图是我这些年攒下的经验坐标:

quadrantChart
    title "视觉收益 vs 采样成本:常见材质技巧坐标"
    x-axis "低成本" --> "高成本"
    y-axis "低收益" --> "高收益"
    quadrant-1 "确认主角再上"
    quadrant-2 "闭眼做"
    quadrant-3 "可做可不做"
    quadrant-4 "闭眼砍"
    "湿度 Lerp": [0.2, 0.85]
    "顶点色混合": [0.25, 0.7]
    "细节法线": [0.35, 0.75]
    "ORM 通道打包": [0.15, 0.6]
    "世界空间苔藓": [0.5, 0.55]
    "三平面映射": [0.7, 0.6]
    "视差遮蔽 POM": [0.9, 0.65]
    "无目的多层噪声": [0.75, 0.3]
    "同一贴图重复采样": [0.6, 0.12]

左上角的「湿度 Lerp、顶点色混合、细节法线、ORM 打包」是闭眼做的:成本低、收益大。右下角的「同一贴图重复采样、无目的的多层噪声」是闭眼砍的。右上角的视差遮蔽(POM)和三平面映射属于「确认它是视觉主角再上」——一面玩家会贴着看的石墙值得 POM,一面远观的墙用视差就是烧钱。材质优化的乐趣不在砍,在于知道哪笔钱花得值。

8.5 效果拆解三则:物理主项比参数微调值钱

理论说够了,拆三个实战效果。每个给关键节点组合和「为什么这样组合」——组合背后的物理判断比节点本身值钱,节点谁都会连,判断就是分水岭。

选这三个不是随机点名,它们各代表材质工作里的一类问题。湿润地面代表「参数混合」:不换模型,靠 Lerp 在干与湿之间连续插值,是环境材质的日常。布料代表「换 Shading Model」:当现象超出默认微表面模型的假设,正确答案不是加节点而是换模型。皮肤代表「成本分级」:同一张脸在特写和远景该用不同档的方案,把预算花在镜头真正在乎的地方。以后遇到任何新效果,先问它属于哪一类,套路就能复用。

湿润地面:颜色只是伴生效果,主项是 Roughness

先拆现象:水填平了地面的微观孔隙,三件事同时发生——粗糙度骤降出现镜面反射、颜色变深(水膜让光在表层多次散射,吸收增加)、低洼处积水成镜。

对应的关键节点组合:

flowchart LR
    A[MPC_Weather.Wetness
0 到 1] --> F[MF_WetnessBlend] B[高度图低值
生成水洼遮罩] --> F C[干 Roughness] --> F D[干 BaseColor] --> F F --> G["Roughness: Lerp 干值到 0.1
水洼区压到 0.02"] F --> H[BaseColor 乘 Lerp 1 到 0.6
整体压暗] F --> I["Specular: 水洼区拉满"]
  • Roughness:Lerp(干值, 0.1, Wetness)。这是主项,湿的物理本质是「微表面被水填平」。
  • BaseColor:干色 × Lerp(1.0, 0.6, Wetness)。压暗是副项,幅度别超过 40%,再多就是泥巴。
  • 水洼:用高度图低值生成遮罩,遮罩内 Roughness 直接压到约 0.02,Specular 拉满。水洼是「另一层介质」(水面),必须单独处理——它跟「更湿的表面」完全不是一回事。

高度图自动生成的水洼解决了「对不对」,美术还要解决「好不好看」。工程上的做法是再叠一层顶点色(Vertex Color)遮罩:美术用网格绘制工具手工刷出想要的水洼位置,和高度遮罩相乘。自动遮罩保证物理合理,手工遮罩保证构图需要——镜头正对着的那块水洼反射篝火,就是靠手刷出来的,高度图不会自己懂构图。

只压暗不调粗糙度是最常见的错误——出来的是「脏」。「湿」?差得远。这个材质函数写成伪代码长这样:

# MaterialFunction: MF_WetnessBlend (伪代码)
# 输入: 干材质属性 + 湿度 + 水洼遮罩; 输出: 混合后的属性
def mf_wetness_blend(color_dry, rough_dry, wetness, puddle_mask):
    # 主项: 水膜填平微表面, 粗糙度向 0.1 靠拢
    rough = lerp(rough_dry, 0.1, wetness)
    # 副项: 水膜多次散射吸收, 颜色压暗, 幅度不超过 40%
    color = color_dry * lerp(1.0, 0.6, wetness)
    # 水洼区: 另一层介质, 直接按水面处理
    rough = lerp(rough, 0.02, puddle_mask * wetness)
    spec  = lerp(0.5, 1.0, puddle_mask * wetness)
    return color, rough, spec

Wetness 挂在 MPC 上,由天气系统统一驱动。下面这段解决「下雨时全场景同步变湿」的问题:

// 天气导演: 降雨强度变化时调用, 全场景材质同帧响应
void AWeatherDirector::SetRainWetness(float Wetness)
{
    if (!WeatherMPC) return; // 编辑器里指定的参数集资产
    // 标量参数: 所有引用 MPC_Weather.Wetness 的材质立即生效
    UKismetMaterialLibrary::SetScalarParameterValue(
        this, WeatherMPC, FName("Wetness"), Wetness);
    // 顺带广播风强, 布料材质和植被一起用
    UKismetMaterialLibrary::SetScalarParameterValue(
        this, WeatherMPC, FName("WindStrength"), WindStrength);
}

收益就是 8.3 节说的全局广播:改一个值,整个峡谷的岩石、路面、树叶同一帧变湿,不用碰任何一个材质实例。

布料:换 Shading Model,别硬凑

现象:布料由纤维织成,视线贴近表面的掠射角处,纤维反光形成一圈「边缘亮绒」(sheen),且几乎找不到锐利高光。

关键节点组合:Shading Model 切到 Cloth;Roughness 给 0.7–0.9;Fuzz 贴图(纤维噪声纹理)接 Cloth 输入;需要方向感时再加切线贴图做各向异性。

为什么这样组合:Default Lit 的微表面模型假设表面由均匀小镜子组成,而布料的边缘光是纤维圆柱体的散射,物理路径根本不同。用默认模型硬凑,Roughness 调到天荒地老也只是「发亮的纸」。换 Shading Model 是一行选择的成本,收益是布料一眼可信——旗帜、窗帘、角色外套这类视觉主角配得上它;场景里所有小物件都用,就是浪费指令,Cloth 模型的指令开销比普通 Lit 高一截。

皮肤:发红的塑料?不,这是半透明介质

现象:光射入皮肤,在内部散射后从别的位置射出——次表面散射(Subsurface Scattering,SSS)。逆光看自己的指缝和耳垂,那层透出来的红就是它在说话。

关键节点组合:Shading Model 切到 Subsurface(主角用 Subsurface Profile,便于全项目共享配置);Subsurface Color 接偏红橙的散射色;Roughness 保持 0.4–0.6(皮肤有油脂微光,不是全哑光);细节法线负责毛孔。Metallic 恒为 0,Specular 保持默认 0.5(对应 F0≈0.04)——乱调 Specular 会让脸泛出金属光。

为什么必须走光照路径:SSS 发生在光照计算里,光线换方向,透光的位置和强度跟着变。把 BaseColor 调红来「模拟血色」是另一个常见错误——那是把散射结果预烘焙进反照率,光一变向就糊,而且糊得毫无救药。

成本分级照抄即可:主角特写用 Subsurface Profile,NPC 中景用 Subsurface,远景和尸体切回 Default Lit。三米以外没人看得出区别,省下的填充率是实打实的。

8.6 美术补课·色彩分工与「回响峡谷」的调色板纪律

补课:本节补美术基础——色彩三要素在场景中的分工。美术出身可跳过;程序出身请读完,材质翻车一半以上节点连错?不,多数是颜色关系错了。

色彩有三个可独立控制的维度,它们在场景里的分工完全不同。

明度(Value) 是画面的骨架。人眼先读明暗,再读颜色。场景的远近层次、视觉重心、引导路径,七成靠明度差完成。自检方法极简单:把截图去色,层次还在,明度设计就过关;去色后糊成一坨,多半是「用颜色做层次」的典型错误。

饱和度(Saturation) 是情绪的音量。高饱和对应欢快、紧张、卡通感;低饱和对应疲惫、肃杀、纪实感。末日题材的第一反应就该是压饱和——拿《最后生还者》的画面去测,大面积环境的饱和度很少超过三成。

色温(Color Temperature) 是氛围的时间戳。冷色(蓝青)传达夜、未知、危险将至;暖色(橙黄)传达安全屋、篝火、回忆段落。但注意分工纪律:材质管反照率,灯光管色温。把冷蓝色调画进墙面贴图,灯光师后面怎么调都是脏的——布光是第 10 章的活。材质这边的职责是保持中性的反射率,把色温留给光照系统。

落到「回响峡谷」,调色板原则一句话:低饱和铺底,单点强调色点睛。整套材质包的组织长这样:

flowchart TD
    A["M_Master_Environment
主父材质: 三贴图 + Wetness 开关 + 细节法线"] --> B1[MIC 岩石 ×3] A --> B2[MIC 混凝土 ×2] A --> B3[MIC 锈蚀金属 ×2] A --> B4[MIC 植被 ×4] F1[MF_WetnessBlend] -. 内联 .-> A F2[MF_DetailNormal] -. 内联 .-> A F3[MF_WorldMoss] -. 内联 .-> A C[MPC_Weather] -. 全局参数 .-> A D[M_Accent_Orange
自发光强调色] -. 仅用于 .-> E[安全屋灯带 / 收集品]

具体配额:

颜色家族饱和度明度职责
环境底色(蕨林、铅灰天空、褪色混凝土)15%–30%30–45存在但不说话
强调色·橙(安全屋灯带)70%+60+唯一的安全信号
强调色·蓝(收集品微光)70%+55+与橙二选一的场景主提示

三条执行纪律。第一,环境贴图整体压饱和,它们的任务是让眼睛休息。第二,强调色的物理载体是独立小材质加 Emissive 自发光,绝不混入环境贴图——混进去就没法统一管理,也没法在剧情需要时整体熄灭。第三,强调色全场景同时只保留一个家族:低饱和环境里,一个高饱和点的视线引力相当于三盏聚光灯;给五个物体用强调色,等于谁都没用。策划要的「玩家一眼看到可交互物」,靠的是这个配额的严格执行,不是靠 UI 箭头。

明度配套着用:峡谷整体压在中低明度区,安全屋拉到 60 以上,用明度差做「回家」的引导。玩家未必说得出为什么,但他的脚会自己走向亮处。

这套纪律的日常工作流三步:先定情绪板——从参考图里取色定下底色盘的五个主色,写进调色板文档,任何人做新贴图先从这五个色域里取;每周去色检查——挑三张本周截图去色,视觉重心跑偏就查明度;每次提审饱和度审计——用引擎的取色器抽测环境贴图,超过 30% 的打回。听着繁琐,执行起来就是每周十分钟的例会动作,换来的是整个项目不用开「画面为什么又花了」的复盘会。

小结

PBR 四参数背定义没用,极端值各打一次 0 和 1,直觉就长出来了;Metallic 的正确形态是遮罩图,不是滑杆。材质图是数据流程序:节点数不代表成本,纹理采样次数和指令数才是账单;Static Switch 拿 cook 时间换运行时,动态分支尽量换成无分支数学。复杂度靠三件套关押:MaterialFunction 管复用(记得冻结叶子函数)、MIC/MID 管变体、MPC 管全局广播——单物体状态走 MID,全场景状态走 MPC,别混。湿、布、皮三个效果的共同教训:先切对 Shading Model、抓住物理主项(Roughness、SSS),再谈参数微调;主项不解决,调颜色永远得到「脏」而不是「像」。色彩分工上,明度做骨架、饱和度做音量、色温归灯光。「回响峡谷」用低饱和铺底加一个强调色家族做视线引导。

上手任务

  1. 极端值实验台:建一个测试关卡放 2×2 四个球,Roughness/Metallic 分别取 0 和 1 的全部组合,BaseColor 统一中灰,只开一盏点光。验收标准:不看细节面板,能凭画面说出每个球的参数组合,且能解释金属球的高光为什么带颜色。
  2. 湿度材质落地:为「回响峡谷」灰盒地面实现 MF_WetnessBlend,Wetness 挂到 MPC。验收标准:Wetness 从 0 拉到 1 的过程中,干燥→湿润→水洼三阶段过渡连续,水洼处能看到天空的镜面反射,且场景内所有引用该函数的材质同帧响应。
  3. 材质体检:用 Shader Complexity 视图(Alt+8)扫一个已有场景,找出指令数最高的三个材质,记录它们各自的 Texture Sample 数。验收标准:输出一页排查报告,至少落地一项优化——砍掉 2 次重复采样,或把 1 张环境 4K 贴图降到 2K,并记录优化前后的纹理池占用变化。
  4. 调色板审计:截一张场景图,分别去色和提取饱和度通道。验收标准:去色后视觉重心仍落在安全屋;饱和度通道里只有强调色物体发亮,环境区域全部低于 30%。

下一章

材质让表面可信,但「回响峡谷」现在还只是个不能交互的漂亮盒子。第 9 章把它变成竖切原型:灰盒尺寸的门道、移动手感调参、调试兵器库,以及卷一的验收清单。

延伸阅读

  • 官方文档:Unreal Engine → Rendering → Materials(Material Inputs、Shading Models、Material Functions、Material Parameter Collections 四个词条连读)
  • 官方文档:Unreal Engine → Rendering → Textures(Texture Streaming 与 Virtual Texturing 词条)
  • SIGGRAPH 2012:Brent Burley, "Physically Based Shading at Disney"——PBR 参数化的源头,Roughness 平方惯例就来自这里
  • 源码:Engine/Shaders/Private/BRDF.ush——UE 里 GGX 的真实实现,比论文短得多
  • 主题方向:The Order: 1886 团队关于 PBR 材质管线落地的 GDC/SIGGRAPH 分享(搜 "Order 1886 physically based shading"),看 3A 团队怎么做反照率校验流程