这不是一次“把车漆调亮”的材质练习,而是一次从通用 Shading Model 到项目生产 Shader 的完整落地。我保留 Unity HDRP StackLit 的 VLayered BSDF 核心,围绕车辆资产重新构建材质输入、前向光照接入、反射控制、损伤与雨水模块以及自定义 Shader GUI;随后又把同一套车漆推入全量延迟路径,验证复杂分层材质在 GBuffer 中需要付出什么代价。
Unity 2022.3 自研 HDRP 分支
3-Layer Base / Flake / Clear Coat
Forward + Deferred 两条完整光照路径
4 × RGBA16 全量延迟扩展 GBuffer
最终延迟版本。高光层、环境反射、几何法线与 SSR 已恢复到接近前向参考的表现。
问题不是“亮不亮”,而是层次是否成立
真实车漆的识别度来自多个尺度同时成立:底漆决定颜色和大尺度反射,金属颗粒打碎近景高光,最外层清漆提供更锐利的第二套法线与 Fresnel。只提高 Smoothness 会得到像塑料一样的单层亮面;简单叠加两个 Specular,又会在高亮区域破坏能量关系。
HDRP StackLit 已经提供了适合这个问题的物理骨架,但原版是面向通用材质系统设计的。项目真正需要的不是把全部 StackLit 选项搬进一个面板,而是确定视觉核心、建立资产约定,并让 Shader 能进入车辆灯光、损伤、天气、SSR 和渲染管线。
先做横向拆解:成品车漆依赖哪些输入
在决定 Shader 功能之前,我先对《异环》《The Crew 2》《Cyberpunk 2077》和《Forza Horizon 5》的公开运行画面与调试截帧做了横向观察。这里必须区分“看见的数据”和“推断的实现”:截帧可以确认某类贴图、遮罩或材质层存在,却不能证明对方使用了某一段具体 BRDF 代码。对标的价值不是复刻别人的参数,而是建立资产需求清单。
参考画面的 Coat 扫描。高光形状与环境反射是两个相关但不能混为一个滑杆的视觉维度。
参考对象
截帧中可确认的输入
对项目方案的启发
《异环》
车身法线、Base Color、Emission、Flake Normal、损伤、Cubemap 与打包 Mask
车漆不是单个颜色参数,基础几何细节、颗粒、局部状态和环境响应必须分层
《The Crew 2》
车身法线、损伤、脏迹法线、碳纤维与局部 Mesh/Mask
不同部件需要共享主模型,同时允许局部材质和损伤通道独立
《Cyberpunk 2077》
运行画面中的多材质分区、表面磨损与局部覆盖
视觉复杂度更多来自区域语义和状态叠加,不应只依赖更复杂的高光公式
《Forza Horizon 5》
Damage、Flake Normal、多层脏迹、Cubemap、Normal 与水迹方向
高质量车辆材质需要把“干净车漆”和天气/损伤输入设计成同一数据协议
参考资产输入节选。截图用于功能归类,不把画面中的通道布局直接当作对方引擎的最终材质协议。
这轮拆解直接改变了实现优先级:先保证 Base/Normal/Mask、Flake、Coat、Damage/Weather 和 Reflection 六类数据有稳定入口,再讨论更昂贵的波瓣或延迟编码。它也暴露了一个常见误区:环境反射质量高度依赖 Cubemap、Probe、SSR、GI 与场景曝光,不能把所有差异都归因到车漆 Shader。
03 Clear Coat
独立光滑度、IOR、厚度、消光色和法线;控制锐利高光与掠射反射。
02 Flake / Pigment
高频法线扰动与副波瓣,负责近景颗粒和高光破碎。
01 Base Coat
底色、金属度、AO、主法线和主波瓣,提供稳定的大尺度材质形体。
先拆原版 StackLit:数据如何走到光照
我没有重写 StackLit 的主体 BRDF。自定义版本与项目内原版 StackLit.hlsl 仍保持绝大部分代码一致,核心工作是理解并保留它的数据契约。阅读顺序不是从四千多行文件的第一行一路向下,而是沿一条稳定的数据流:
Material Inputs 贴图与参数
→
SurfaceData 材质语义
→
BSDFData 光照参数
→
PreLightData 视角相关预计算
→
LightLoop Direct / SSR / IBL
双波瓣不是“两次高光相加”
StackLit 的底层可以同时持有两组 GGX 波瓣。每组波瓣有自己的粗糙度,最后通过 lobeMix 混合。开启各向异性时,标量粗糙度会进一步展开为切线和副切线方向的粗糙度。它解决的是同一底层材料中宽高光与窄高光共存的问题,而不是清漆层。
baseSpecular = NdotL * bottomF
* lerp(DV_LobeA * energyA,
DV_LobeB * energyB,
lobeMix);
Clear Coat 使用 VLayering 处理能量
清漆位于底层上方。StackLit 的 ComputeAdding 会根据 Coat IOR、厚度、消光和观察角度,把顶层反射、底层透射以及等效粗糙度组合进 PreLightData。直接光阶段最终同时计算底层波瓣与 Coat 波瓣,但 Coat 不只是额外加亮,它还会改变进入底层的能量。
coatSpecular = coatNdotL
* topLayerEnergy
* coatEnergyCompensation
* coatDV;
finalSpecular = baseSpecular + coatSpecular;
这也是车漆不能简单退回普通 Lit Clear Coat 的原因:这里不仅有独立 Coat,还要保留双底层波瓣、各向异性、HazyGloss、独立法线以及 SSR/IBL 的逐波瓣权重。
受控矩阵:为什么 Metallic + Coat 暗部更敏感
文档里最有价值的实验,是在固定模型、灯光、曝光和相机后,把 Metallic 与 CoatMask 分别按 0、0.5、0.75、1 扫描,并单独观察 Diffuse、Specular、Reflection 与 GI。它把“暗部发黑”从主观观感拆成了可定位的数据链。
同一组 Metallic × CoatMask 参数分别查看四个光照分量。左上角是低 Metallic/低 Coat,右下角是高 Metallic/高 Coat。
源码中的关系可以简化为三步。首先,金属工作流让 diffuseColor 随 Metallic 增大而趋近于零;其次,VLayering 在 ComputeAdding 中通过 lerp(1, Ti0, coatMask) 计算底层漫反射能量;最后,烘焙漫反射会继续乘上 diffuseFGD × diffuseEnergy,GTAO 的多次反弹着色也使用经过这层能量处理的漫反射颜色。当 Metallic 和 CoatMask 同时接近 1,而 Probe/SSR/GI 又不足以补回镜面环境能量时,暗部自然会非常深。
这不是简单的“模型错误”
StackLit 在表达多层介质的能量关系,结果是否好看取决于材质参数与环境光照是否匹配。线性地把 Lit、UE Clear Coat、StackLit、Substrate 排成“物理精确度排行榜”过于武断,因此我没有沿用原文那张排序图;更可靠的结论是比较每个模型的参数语义、层间透射近似和项目约束。
现象
优先排查
工程处理
正面暗、掠射角正常
Coat Fresnel、正面反射 Remap、曝光
先校准 Probe/SSR,再限定正面反射下限
高 Metallic 区域接近纯黑
Base Color/F0、环境反射覆盖、GI
保证金属区有可反射环境,不用漫反射补假亮度
Coat 开启后整体压暗
diffuseEnergy、Coat IOR/Mask、Extinction
用受控矩阵确定能量缩放范围,并锁定危险组合
缝隙或接触区脏黑
SSAO/GTAO、几何法线、Diffuse tint base
分离查看 AO 与 Beauty,避免 AO 在近零底色上二次压黑
从通用模型到项目级 CarPaint
原版 StackLit 负责“光应该怎么计算”,项目 CarPaint 负责“车辆资产如何稳定地提供这些数据”。我把这部分拆成独立的 CarPaint_Input.hlsl、Forward Pass、Depth/Motion Pass、损伤和雨水模块,再将结果送入 StackLit 的 ConvertSurfaceDataToBSDFData 与 LightLoop。
项目模块
输入或处理
进入 StackLit 的结果
Base / Mask
BaseColor、Metallic、AO、Smoothness 通道选择与 Remap
稳定的底层颜色、F0 与粗糙度
Dual Lobe / HazyGloss
Smoothness A/B、LobeMix,或 Haziness/HazeExtent
两组底层 GGX 波瓣
Flake
高频法线、独立缩放和强度
打碎底层高光,不改变几何轮廓
Clear Coat
Mask、Smoothness、IOR、Thickness、Extinction、独立 Normal
VLayered 顶层界面
Reflection Tuning
直接光高光大小/强度/颜色、IBL 粗糙度解耦、正面/掠射 Remap
在物理骨架上提供车辆美术控制
Damage / Weather
8 区域损伤、划痕、泥点、灰尘、雨痕与水滴
联动颜色、法线、AO、Smoothness 与 Coat 移除
Vehicle Lighting
车辆专用灯光倍率、四通道车灯 Emission
接入项目灯光分层和运行时状态
前向路径:直接保留完整材质状态
前向版本是这套 Shader 最直接、数据最完整的实现。Fragment 阶段构建 SurfaceData,转换为 BSDFData,采样 Baked GI,再生成 PreLightData。随后我在进入 LightLoop 前对 Coat 做项目级调制:
_CoatSpecularSize 调整直接光 Coat 波瓣粗糙度。
_CoatSpecularIntensity × _CoatSpecularColor 调整顶层直接光能量。
_CoatReflectionMin / Max 重映射 Coat 的环境反射响应。
Reflection Override 将直接光 Smoothness 与 IBL/SSR 使用的反射粗糙度解耦。
车辆灯光倍率在 Punctual 与 Area Light 回调中逐灯生效。
前向参考。材质常量直接参与 PreLightData 和 LightLoop,不需要经过 GBuffer 量化与重建。
Shader GUI 是生产系统的一部分
复杂 Shader 如果只提供一长列属性,很快会失去可维护性。配套的 CustomShaderGUI_CarPaint 按材质职责组织折叠面板,并由 Toggle 同步 Shader Keyword。常用参数直接暴露,容易破坏资产一致性的高级参数默认锁定,同时提供一键恢复默认值。
基础与车漆颜色 法线与 Mask 双波瓣高光 清漆层 Flake 反射与 Fresnel 车灯 Emission 区域材质 损伤与污渍 雨水 破碎玻璃 渲染状态
GUI 里最重要的并不是“参数多”,而是把依赖关系说清楚。例如启用 HazyGloss 后,Smoothness B 与 LobeMix 不再由美术直接填写;启用 Reflection Override 后,面板明确区分“直接光 Coat 光滑度”和“环境反射清晰度”;损伤模块则按固定区域 ID 管理局部状态,避免运行时为每个部件复制材质。
参数矩阵把 GUI 约束变成资产规则
单张 Beauty 很难判断参数是否正交,所以我又建立了球阵列测试:分别扫描 Base Smoothness、Metallic、CoatMask、CoatSmoothness、Coat IOR、Thickness 与 Extinction。矩阵的作用不是展示“参数很多”,而是找出哪些参数可以交给美术自由调整,哪些必须成组出现,哪些应该被预设锁定。
参数矩阵节选。每组测试只改变标题中的变量,避免场景曝光和灯光变化掩盖材质响应。
参数组
观察结论
GUI 策略
Metallic / Base Smoothness
Coat 关闭时遵循常规金属度 PBR 语义
作为基础材质参数直接开放,并提供贴图通道 Remap
CoatMask / CoatSmoothness
前者控制顶层参与度,后者控制顶层波瓣宽度,两者不能合并
常用区开放,但用预设限定有效范围
Coat IOR / CoatSmoothness
IOR 同时改变 F0 与角度响应,不只是“高光强度”
归入高级参数,默认锁定在材质类别预设中
Thickness / Extinction
两者共同决定颜色吸收;厚度接近零时 Extinction 基本失去意义
成组显示、联动启用,避免单独调色造成不可解释结果
Environment / Reflection
决定暗部是否有可信的镜面信息,但主体数据来自场景
材质仅保留必要的粗糙度与范围调制,不用局部 Cubemap 掩盖环境缺失
最终资产规则因此比“提供一套默认材质球”更具体:车身至少要有可用的 Base/Normal/Mask,近景车型补 Flake Normal,需要状态变化时再接 Damage、Dirt 与 Water;Coat IOR、Thickness 和 Extinction 由材质类别预设管理。这样既保留 StackLit 的物理层次,也避免每辆车都从几十个自由参数重新试错。
为什么继续尝试全量延迟
前向效果已经成立,但项目场景主体走 Deferred,车漆需要更自然地接入统一的深度、法线、SSR、灯光分层与调试链路。问题是普通 HDRP GBuffer 只为 Lit 的材质模型设计,无法完整表达 StackLit 的双波瓣、VLayer Coat、几何法线、各向异性和项目反射控制。
因此我没有让延迟版本使用一套简化 BRDF,而是要求它恢复出与前向相同的 BSDFData 和 PreLightData。流程变成:
Depth / Stencil 可见性与材质标记
→
HDRP GBuffer 公共 Lit 数据
→
CarPaint Data 4 张扩展 RT
→
Decode 重建 BSDFData
→
StackLit LightLoop 同一套光照核心
四张扩展 GBuffer 保存什么
CarPaintGBufferData 是一个独立几何 Pass,在公共 GBuffer 之后写入四张全分辨率 R16G16B16A16_UNorm。使用独立 Pass 是因为 HDRP MRT 还可能被 VT Feedback、Light Layers 和 Shadow Mask 占用,无法稳定追加完整车漆数据。
RT
主要内容
设计目的
CarPaint0
Base Normal、Roughness A/B、Tangent Angle
恢复双波瓣与各向异性坐标系
CarPaint1
Geometry Normal、Lobe/Hazy、Coat IOR、Anisotropy
恢复 VLayering、几何约束与功能分支
CarPaint2
Coat 高光控制、反射范围、Reflection Override、能量与厚度
恢复项目级 PreLightData 调制
CarPaint3
Coat Specular RGB、Extinction RGB、Env Grayscale
恢复完整顶层颜色和介质衰减
每个 16-bit 通道进一步保存两个独立 8-bit 数据。法线使用 Octahedral 编码,切线压缩为相对法线局部坐标系中的一个角度;HazyGloss 则复用在该模式下本来失活的 Lobe B 槽位。这里的目标不是追求极限压缩,而是先建立一份能够与前向逐项核对的“全量基线”。
效果对齐不是一次完成的
第一版延迟虽然能正确受光,但反射明显弱于前向,几何法线也没有真正参与最终层状计算。原因不在单一公式,而在多个阶段共同作用:Coat FGD 被项目反射重映射后又被 SSR 使用、SSR Alpha 同时承担反射层级权重、fullscreen shader 的功能全集改变了法线数组布局,而一些前向材质常量没有完整进入 GBuffer。
初版 Deferred:反射层次与高光能量不足。
Forward Reference:Coat 和环境反射更完整。
最终修正包括:保留 SSR 使用的原始 Coat FGD、恢复 Geometry Normal 与 Coat/Base Normal 的正确槽位、让弱 SSR 不再完全吞掉 Env/Sky/Probe、统一 SSR 高亮压缩,并将前向的 Coat Specular、Extinction、反射粗糙度与能量参数完整编码。
一次典型的跨阶段 Bug:白色为什么变成紫色
最终阶段出现过一个非常有代表性的错误:_CoatSpecularColor 明明是白色,移动灯光时 Coat 高光却呈现紫色。修改 Coat 颜色会改变紫点,因此问题一定在 Coat 直接光链路,但材质常量本身没有偏色。
错误的 byte-pair 打包污染了 RGB 通道,紫色高光随后又进入下一帧 SSR Color Pyramid。
根因
公共 Pack2Byte 在高位值仍带小数时先乘 256,最后才取整。对 Coat 白色而言,1.0 / 8.0 应打包为字节 (32, 32),实际却形成 (32, 0)。RGB 解码结果约为 (1.004, 0, 1.004),正好呈紫色。
修复方式是在移位之前先分别把两个值量化到 0–255,再组合为精确 16-bit 整数,并使用 R16_UNorm 保存。GPU 回读验证白色为 (32,32,32),0.8 灰色为 (26,26,26)。这个问题也说明:Deferred 的调试不能只看最终 Beauty,必须能检查 GBuffer 原值、解码值、PreLightData 和 SSR 输入。
Forward 与全量 Deferred 的真实取舍
维度
Forward
全量 Deferred
数据精度
材质常量直接进入 BSDF,无额外量化
复杂参数需要编码、量化和重建
效果一致性
基准实现,透明和特殊功能自然接入
修正后接近前向,但 SSR 层级与量化仍非数学等价
几何与材质成本
一次主要材质计算与 LightLoop
公共 GBuffer、额外数据 Pass、fullscreen lighting
显存与带宽
不需要车漆专用 RT
额外 4 × RGBA16,即 32 Byte/Pixel
管线集成
需要维护 Forward 对公共深度/法线的写入
更容易接入统一 Deferred、SSR 与屏幕空间调试
扩展维护
增加参数后可直接读取
每个新参数都要重新审视 GBuffer 协议
以 1920×1080 计算,四张扩展 RT 额外占用约 63.3 MiB;只算一次写入和一次读取,理论流量约 126.6 MiB/帧。4K 下分配约 253 MiB。实际带宽会受到车辆覆盖率、压缩和 GPU 架构影响,但额外几何 Pass 与全尺寸资源是真实存在的。
现代 HDRP Forward 已使用灯光列表,并不是传统的“每盏灯增加一次 DrawCall”。因此这次全量延迟的主要收益是统一数据链路和验证技术边界,而不是自动获得性能优势。对于当前车漆,Forward 仍然是性价比更高的生产路径;全量 Deferred 更适合作为可验证基线,下一步应尝试只保存公共 GBuffer 无法重建的数据,将四张扩展 RT 收敛到一到两张。
这项工作的最终价值
这个案例的重点不是“使用了 StackLit”,而是把一个通用、庞大的物理材质模型变成可由车辆资产稳定使用的生产系统。我完成了从源码阅读、材质语义、前向 LightLoop、反射层级、项目灯光,到 GUI、损伤、天气和全量延迟实验的整条链路。
工程结论
保留经过验证的 StackLit VLayering 核心,把项目差异集中在 SurfaceData、PreLightData 与渲染管线边界;先建立效果正确的 Forward 基准,再用全量 Deferred 暴露数据需求,最后根据真实性能决定是否压缩,而不是为了“走延迟”牺牲可解释性。
原始分析资料
25 页原始 PDF 保留了参考作品截帧、资产输入、源码思维导图、暗部能量拆解和材质参数矩阵。网页文章在此基础上校正了过度简化的模型排序,并补充项目 CarPaint 前向实现、Shader GUI、全量延迟设计与完整问题复盘。
打开原始 PDF:车漆技术分析