从 LM-63 到实时预览:Unity IES 光度文件编辑器

IES 文件保存的不是一张灯光纹理,而是灯具在不同空间方向上的光强分布。这个工具的目标,是让技术美术和灯光美术无需离开 Unity,就能读取 IESNA LM-63、检查光度曲线、观察真实落地形状、编辑能量分布,并把结果重新保存成可被其他渲染器继续使用的标准文件。

Unity IES 光度文件预览与高级曲线编辑界面
工具同时展示光度学数据与可编辑曲线。所有截图均来自独立测试资源,不包含项目场景、业务资产或内部路径。
IESNA LM-63文本解析与标准回写
C-γ Table二维坎德拉分布
2D + 3D曲线、投影与空间预览
Editor Only不引入运行时成本

问题:一份 IES 文件为什么难以直接判断

传统的纹理预览适合颜色和法线,却不适合 IES。IES 记录的是光强 I(C, γ)γ 表示相对灯具光轴的垂直角,C 表示绕光轴旋转的水平角。一个值的单位是 candela,它描述“向这个方向发出多强的光”,而不是屏幕上的最终亮度。

因此,工程里真正需要回答的是三类问题:文件是否有效,光束在角度域里是什么形状,以及它打到真实表面后会形成怎样的照度。只有极坐标图会缺少空间直觉;只把 IES 挂到场景灯光上,又会被曝光、材质、天气和渲染管线干扰。工具把这三层拆开,分别提供数据、物理投影和独立 3D 预览。

LM-63 TextHeader / TILT / Angles / Candela

IESDataC-γ 二维光强表

PreviewPolar / Surface / 3D

EditScale / Spread / Shift / Curve

Serialize重算峰值与光通量

第一层:把 LM-63 解析成可工作的光度数据

解析器先读取版本头与方括号关键字,直到遇到 TILT=;随后把换行、空格、制表符和逗号统一视为数值分隔符。这样可以兼容不同厂商导出时的折行差异。数值读取使用 InvariantCulture,避免编辑器系统语言改变小数点语义。

数据段 主要字段 工具中的用途
Header / Keywords LM-63 版本、灯具名称、厂商描述 资产识别与保存时保留元数据
Photometric header 灯数、流明、倍率、角度数量、Type、单位和灯具尺寸 校验文件、显示摘要、确定数组尺寸
Vertical angles γ[0..V-1] 描述离开光轴的采样角
Horizontal angles C[0..H-1] 描述绕光轴的采样平面与对称性
Candela table cd[H,V] × multiplier 预览、编辑与保存的核心数据

读取坎德拉表时,原始 multiplier 会立即乘进每个样本,内部始终使用绝对光强。同时遍历整张表得到峰值 candela 及对应的 (C, γ),后续的归一化、曝光参考和光束角计算都依赖这一组统计数据。

TILT 的处理边界

解析器能够跳过文件内嵌的 TILT 数据,使预览不会因额外数值段错位;但编辑和保存只允许 TILT=NONE。这是刻意的防御性约束:没有完整实现倾斜灯具的变换语义前,不应该写出看似合法、实际方向错误的文件。

第二层:极坐标图把“光往哪里走”画出来

极坐标图以灯具光轴为 0°,绘制 C0 和 C90 两个典型截面。半径表示光强,角度表示 γ。线性模式适合比较绝对能量,Log 模式能把弱尾部拉出来检查;Normalize 则让不同功率的灯具可以只比较光束形状。

IES C0 与 C90 平面的极坐标光度曲线和元数据
极坐标曲线与文件摘要并排显示。示例为轴对称分布,因此 C0 与 C90 重合;工具仍明确标记两个光度平面的语义。

Beam Angle 和 Field Angle 不是额外存储的字段,而是从峰值向两侧寻找阈值交点得到的派生指标。工具分别使用峰值的 50% 和 10% 作为阈值,用于快速判断主光束宽度与外围有效范围。它们适合资产筛选,但不能替代完整曲线,因为两个光束角相同的灯具仍可能拥有完全不同的肩部和尾部。

第三层:从坎德拉到表面照度

为了回答“打到地面或墙上是什么样”,工具提供纯 CPU 的表面投影。每个像素先从灯具位置构造方向,换算为 (C, γ),再在坎德拉表中做双线性插值。最终照度遵循点光源的基本关系:

E(p) = I(C, γ) · max(0, N · L) / r²

其中 I 是该方向的光强,N · L 是表面入射余弦, 是平方反比衰减。预览再用峰值和灯距做自动曝光,并经过指数型 tone mapping 与 sRGB 转换。这样既保留光束形状,又不会让不同量级的 IES 在编辑器里直接全白或全黑。

对称性展开是正确采样的关键

IES 文件为了节省数据,常常只保存部分水平角:一行代表轴对称,0° 到 90° 代表四象限对称,0° 到 180° 代表双边对称,接近完整一圈则按全方向数据处理。采样任意 C 角时,工具先按文件声明隐含的对称性折叠到已存区间,再在 C 和 γ 两个方向插值。忽略这一步,预览会出现缺扇区、断边或左右不一致。

第四层:不依赖渲染管线的 3D 实时预览

场景预览没有创建 Unity Light,也不要求 HDRP 的 IES Profile。CPU 先把分布烘焙为一张 360 × 181RGBAHalf 查找纹理,U 对应 C 角 0° 到 360°,V 对应 γ 角 0° 到 180°,R 通道保存相对峰值归一化的光强。

IES 灯具位置方向与地面照明的交互式三维预览
灯具位置、Yaw、Pitch 和曝光可以实时调整;相机支持环绕、缩放和 FOV 修改。预览关注光斑方向与空间尺度,而不是复刻某条项目渲染管线。

GPU 侧通过一次 Graphics.Blit 执行全屏 ray caster。每个像素构造相机射线,与地面平面求交,在灯具局部基底中恢复 C 和 γ,再查询 IES LUT 并计算 I · cosθ / r²。地面网格只用于提供尺度与透视参照。因为整个过程只依赖一个隐藏 Shader 和一张 512² RenderTexture,所以 Built-in、URP 与 HDRP 编辑器环境都能使用同一套预览。

为什么不直接复用项目灯光

独立预览把 IES 本身与场景变量隔离开。它不会受到曝光卷积、阴影、材质 BRDF、体积雾或天气系统影响,因此非常适合判断“文件是否正确”。最终落地仍需回到目标渲染管线验收,两者解决的是不同问题。

编辑:修改的是能量分布,不是灯的 Transform

编辑器始终保留一份 Source,并在每次参数变化时深拷贝出 Edited Snapshot。所有操作都作用于坎德拉表,因此保存后的光束形状可以被其他支持 IES 的软件复现,而不是只在当前 Unity 场景里生效。

IES 基础编辑参数,包括亮度、展宽和二维光束偏移
基础编辑面向高频操作:整体亮度、光束展宽,以及不移动灯具 Transform 的前后/左右光束偏移。
参数 实现方式 结果
Brightness 所有采样乘以全局 Scale Factor 保持形状,改变绝对 candela
Beam Spread 按比例反向重采样 γ 角距离 大于 1 展宽,小于 1 收窄
Push Forward / Sideways 把输出方向旋转回源分布后采样 光轴不动,能量峰值在 IES 内偏移
γ Remap 按垂直角应用 AnimationCurve 倍率 独立调整中心、肩部与远端能量
C0 / C90 Freehand 用归一化曲线覆盖指定光度平面 直接塑造两个关键截面

二维光束偏移是其中最容易被低估的一步。工具先把目标偏移量组合成一个峰值方向,再通过 Quaternion 构造“目标峰值到原始光轴”的旋转。对输出表中的每个方向,先做逆向方向映射,再回到源表双线性采样。这样避免直接平移数组造成角度空间变形。

轴对称文件只有一行水平角,无法表达偏向某个 C 平面的光束。一旦发生非零二维偏移,工具会把它扩展为 0° 到 360°、每 10° 一行的完整表,再进行方向重采样。这个扩展会增加文件数据量,却是打破轴对称后保持信息完整所必须付出的成本。

IES 高级 γ 重映射与 C0 C90 自由曲线编辑
高级模式允许按 γ 角做乘法重映射,并直接编辑 C0/C90 曲线。界面根据轴对称、四象限、双边或全方向数据自动限制不可表达的通道。

保存:不只把数组写回文本

序列化器保留原有关键字,把内部的绝对 candela 写回文件,并强制 multiplier 为 1,避免“数组已经乘过倍率、文件又重复乘一次”的二次放大。保存时还可以追加一条编辑历史说明;界面默认提供 Save As,并把覆盖原文件放在明确的危险操作中。

编辑后的总光通量通过球面积分重新估计:

Φ = ∫₀²π ∫₀π I(γ, C) · sin(γ) dγ dC

离散实现沿相邻 γ 区间积分,并对已存 C 范围求平均,再利用轴对称、四象限或双边对称扩展到完整 2π。结果乘以 Ballast Factor,写回 Lumens Per Lamp。峰值 candela 与峰值角度也会重新遍历计算,保证文件摘要与编辑后数据一致。

模块拆分与职责

模块 职责 设计价值
Parser / IESData LM-63 读取、字段归一化、峰值统计 让所有视图共享同一份明确数据模型
Plot Renderer C0/C90 极坐标曲线、线性/对数显示 快速检查角度域分布
Light Projector 对称性映射、双线性采样、地面/墙面照度 提供不依赖场景的物理形状参考
Scene Previewer + Shader LUT、全屏射线、相机和灯具交互 跨管线提供实时空间反馈
Editor 非破坏重采样、曲线覆盖、对称性扩展 把艺术控制落到标准光度数据
Serializer 元数据保留、流明积分、LM-63 回写 形成可交付、可复用的闭环

性能与工程边界

这是 Editor-only 工具,不进入 Player,也不会给游戏运行时增加 Pass、Draw Call 或常驻纹理。解析与编辑的主要复杂度是 O(H × V);默认 LUT 约 6.5 万个采样,3D 预览是一张固定 512² RT 的全屏 Pass。CPU 表面投影默认 256²,最大限制为 1024²,只有重建时发生。

边界 当前策略 后续可扩展方向
TILT 非 NONE 允许安全读取,禁止编辑保存 实现完整倾斜几何与外部 TILT 文件解析
Photometric Type A/B 保留数据;空间预览按常用 Type C 语义解释 加入 A/B 到 Type C 的坐标转换
异常水平角表 标准 0/90/180/360 对称;其他范围钳制 按 LM-63 边界情况增加更严格校验
高分辨率 CPU 投影 同步生成并限制分辨率 编辑防抖、Jobs/Burst 或 GPU Compute
最终画面一致性 独立曝光与简化地面模型 增加目标管线适配层与基准场景对照

结论

这套工具的价值不在于多做了一个 IES 缩略图,而在于建立了一条完整、可验证的数据链:从标准文件读取方向光强,经过对称性正确的采样与物理照度预览,再以非破坏方式重塑分布,最后重算派生量并写回标准格式。它把灯光资产从“只能导入后试效果”的黑盒,变成可以检查、比较、编辑和交付的数据资产。

对生产工具而言,最重要的并不是把所有边界一次性覆盖,而是明确哪些数据已经被正确解释、哪些操作会改变物理量、哪些格式暂时只读。正是这些约束,让实时反馈不会以牺牲文件可靠性为代价。

项目级车漆:从资产对标、HDRP StackLit 到全量延迟

这不是一次“把车漆调亮”的材质练习,而是一次从通用 Shading Model 到项目生产 Shader 的完整落地。我保留 Unity HDRP StackLit 的 VLayered BSDF 核心,围绕车辆资产重新构建材质输入、前向光照接入、反射控制、损伤与雨水模块以及自定义 Shader GUI;随后又把同一套车漆推入全量延迟路径,验证复杂分层材质在 GBuffer 中需要付出什么代价。

Unity 2022.3自研 HDRP 分支
3-LayerBase / 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。

03Clear Coat

独立光滑度、IOR、厚度、消光色和法线;控制锐利高光与掠射反射。

02Flake / Pigment

高频法线扰动与副波瓣,负责近景颗粒和高光破碎。

01Base Coat

底色、金属度、AO、主法线和主波瓣,提供稳定的大尺度材质形体。

先拆原版 StackLit:数据如何走到光照

我没有重写 StackLit 的主体 BRDF。自定义版本与项目内原版 StackLit.hlsl 仍保持绝大部分代码一致,核心工作是理解并保留它的数据契约。阅读顺序不是从四千多行文件的第一行一路向下,而是沿一条稳定的数据流:

Material Inputs贴图与参数

SurfaceData材质语义

BSDFData光照参数

PreLightData视角相关预计算

LightLoopDirect / 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 暗部更敏感

文档里最有价值的实验,是在固定模型、灯光、曝光和相机后,把 MetallicCoatMask 分别按 0、0.5、0.75、1 扫描,并单独观察 Diffuse、Specular、Reflection 与 GI。它把“暗部发黑”从主观观感拆成了可定位的数据链。

Metallic 与 CoatMask 参数矩阵下的漫反射、高光、环境反射和烘焙 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 回调中逐灯生效。
CarPaint 前向渲染参考效果
前向参考。材质常量直接参与 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。矩阵的作用不是展示“参数很多”,而是找出哪些参数可以交给美术自由调整,哪些必须成组出现,哪些应该被预设锁定。

Lit 与 StackLit 以及清漆遮罩、金属度、光滑度、折射率、厚度和消光参数矩阵
参数矩阵节选。每组测试只改变标题中的变量,避免场景曝光和灯光变化掩盖材质响应。
参数组 观察结论 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 Data4 张扩展 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 直接光链路,但材质常量本身没有偏色。

错误字节打包导致的紫色 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:车漆技术分析