Multi-Layered PBR — Stack-Lit System多层 PBR — Stack-Lit 系统
Layer Stack Concept图层栈概念
NeoX's Multi-Layered PBR implements the Stack-Lit material model (derived from Frostbite's research). Each material is a stack of optical layers: a dielectric Coat layer (like clear lacquer), an absorbing Medium layer (like tinted resin), and the opaque Base material below. Light bounces between each interface according to the Fresnel equations.
NeoX 的多层 PBR 实现了 Stack-Lit 材质模型(源自 Frostbite 研究)。每个材质是一个光学层栈:电介质涂层(如清漆)、吸收性介质层(如有色树脂)和底部不透明基础材质。光线按菲涅尔方程在每个界面间反弹。
The system supports 1, 2, or 3 coat layers via the NB_COAT_LAYERS macro. Each coat adds one normal map texture (t5, t6, t7) and its own IOR, roughness, thickness, and extinction color parameters.
系统通过 NB_COAT_LAYERS 宏支持 1、2 或 3 个涂层。每个涂层添加一张法线贴图纹理(t5、t6、t7)及其 IOR、粗糙度、厚度和消光颜色参数。
Adding-Doubling AlgorithmAdding-Doubling 算法
The physical core of the multi-layer model is ComputeAdding() — it solves the radiative transfer equations iteratively across all layers using the Adding-Doubling method. This correctly accounts for multiple inter-reflections between layer interfaces:
多层模型的物理核心是 ComputeAdding()——它使用 Adding-Doubling 方法在所有层上迭代求解辐射传输方程。这正确处理了层界面之间的多次内部反射:
// Simplified Adding-Doubling loop (from multi_layered_pbr.hlsl) void ComputeAdding(float theta, Material mtl, LightingDataReadOnly data_r) { // Iteratively compose layers from top to bottom for (int i = 0; i < NB_COAT_LAYERS; i++) { ComputeStatistics(cti, 0, i, mtl, ...); // Coat ComputeStatistics(cti, 1, i, mtl, ...); // Medium (absorption) // Adding equations: compose stack so far with new layer m_R0i = (T0i * R12 * Ti0) / (1 - Ri0 * R12); // multiple-bounce contribution e_R0i = R0i + m_R0i; // total reflectance e_T0i = (T0i * T12) / (1 - Ri0 * R12); // total transmittance } // Add base layer (GGX) ComputeStatistics(cti, 2, 0, mtl, fresnel0); // Final effective roughness propagated through all layers data_r.ibl_perceptual_roughness[MODEL_LOBE_IDX] = LinearVarianceToPerceptualRoughness(_s_r0m); }
R_total = R01 + T01²·R_base / (1 - R10·R_base).
为什么用 Adding-Doubling?简单的"乘菲涅尔"方法忽略了在层栈内向上反弹的光。对于汽车漆(清漆覆盖金属底漆),忽略层间反射会在掠射角最多低估 10-15% 的总反射率。Adding-Doubling 通过几何级数精确求解:R_total = R01 + T01²·R_base / (1 - R10·R_base)。
ComputeStatistics — Per-Layer Optical PropertiesComputeStatistics — 逐层光学属性
ComputeStatistics() evaluates one layer's contribution at a given incident angle. It handles the three distinct layer types differently:
ComputeStatistics() 在给定入射角下计算单个层的贡献。它以不同方式处理三种不同的层类型:
| layer_idx层索引 | Type类型 | R12 (reflectance)R12(反射率) | T12 (transmittance)T12(透射率) | Direction change方向变化 |
|---|---|---|---|---|
| 0 (Coat) | Dielectric interface电介质界面 | FresnelUnpolarized(cti, eta, 1.0) | 1 - R12 | Snell's law refraction → computes new cti折射定律 → 计算新 cti |
| 1 (Medium) | Absorbing volume吸收性体积 | 0 | exp(-thickness * extinction / cti) | None (Beer-Lambert only)无(仅 Beer-Lambert) |
| 2 (Base) | Opaque material不透明材质 | F_Schlick(fresnel0, cti) | 0 | None无 |
// Coat layer: full polarisation-resolved Fresnel + refraction angle float FresnelUnpolarized(float ct1, float n1, float n2) { float sinT2 = (n1/n2)*(n1/n2) * (1 - ct1*ct1); if (sinT2 > 1) return 1; // total internal reflection float ct2 = sqrt(1 - sinT2); // Rs = ((n1*ct1 - n2*ct2) / (n1*ct1 + n2*ct2))^2 // Rp = ((n1*ct2 - n2*ct1) / (n1*ct2 + n2*ct1))^2 return 0.5 * (Rs + Rp); // unpolarised average } // Medium layer: Beer-Lambert absorption // thickness in mm; extinction_color is per-channel absorption coefficient float3 T12 = exp(-thickness * extinction_color / cti);
Roughness Variance Propagation粗糙度方差传播
When light refracts through a rough coat layer, the apparent roughness of underlying layers changes. NeoX propagates roughness using linear variance (a representation that makes variances additive):
当光线折射通过粗糙涂层时,下层的表观粗糙度会改变。NeoX 使用线性方差传播粗糙度(该表示使方差具有可加性):
// Conversion functions (stack_lit.hlsl) float RoughnessToLinearVariance(float a) { return pow(a, 1.1) / (1.0 - pow(a, 1.1)); } float LinearVarianceToPerceptualRoughness(float v) { return sqrt(LinearVarianceToRoughness(v)); } // Propagation: combined variance = coat_variance + refracted_base_variance // refracted roughness accounts for the lens-law magnification jacobian: float magnification = (n1 * ct2) / (n2 * ct1); // Snell's law jacobian float refracted_variance = base_variance * (magnification * magnification); float total_variance = coat_variance + refracted_variance;
roughness² directly would be incorrect for non-Gaussian distributions. The pow(a, 1.1) exponent is a practical approximation that makes the GGX NDF variance-additive to within a few percent error across the full roughness range.
为什么用线性方差?在空间频率域,对两个 NDF 波瓣做卷积对应于相加其方差。直接使用 roughness² 对非高斯分布是不正确的。pow(a, 1.1) 指数是一个实用近似,使 GGX NDF 在整个粗糙度范围内以几个百分点的误差内具有方差可加性。
FGD Pre-Integration — ibl.hlslFGD 预积分 — ibl.hlsl
Each coat lobe needs its own FGD (Fresnel-GGX-D) pre-integrated LUT. This is a 2D texture (t4 in all ML-PBR materials) encoding the integrated BRDF over the hemisphere — precomputed at content build time and looked up at runtime:
每个涂层波瓣需要自己的 FGD(菲涅尔-GGX-D)预积分 LUT。这是一张 2D 纹理(所有 ML-PBR 材质中的 t4),在内容构建时预计算并在运行时查找:
// ibl.hlsl — FGD integration (run offline / baking tool) float3 IntegrateGGXAndDisneyDiffuseFGD(float NdotV, float roughness, int sampleCount = 4096) { float3 acc = 0; for(int i = 0; i < sampleCount; i++) { float2 u = Hammersley2d(i, sampleCount); // low-discrepancy sample // GGX importance-sampled contribution acc.xy += ImportanceSampleGGX(u, V, localToWorld, roughness, NdotV, ...); // Disney diffuse contribution acc.z += ImportanceSampleLambert(u, ...); } return acc / sampleCount; // Output: (scale=acc.x, bias=acc.y, disneyDiffuse=acc.z) } // Runtime LUT lookup (multi_layered_pbr.hlsl) void GetPreIntegratedFGDGGXAndDisneyDiffuse(Texture2D tex, float NdotV, float perceptualRoughness, float3 fresnel0, out float3 specFGD, out float diffFGD, out float reflectivity) { float3 preFGD = tex.SampleLevel(s_linear_clamp, float2(NdotV, perceptualRoughness), 0); specFGD = lerp(preFGD.xxx, preFGD.yyy, fresnel0); // scale + bias * f0 diffFGD = preFGD.z + 0.5; reflectivity = Luminance(specFGD); }
| LUT ChannelLUT 通道 | Meaning含义 | Usage用途 |
|---|---|---|
| R (scale) | FGD scale — multiply by F0FGD 缩放系数,乘以 F0 | specFGD = scale * F0 + bias |
| G (bias) | FGD bias — additive termFGD 偏置,加法项 | Handles off-specular at grazing处理掠射角的离轴高光 |
| B (Disney diffuse) | Pre-integrated Disney diffuse lobe预积分 Disney 漫反射波瓣 | diffFGD = preFGD.z + 0.5 |
1 / 2 / 3-Coat Variants1 / 2 / 3 涂层变体
Three material files cover the three coat-layer counts. All are identical in structure except for the NB_COAT_LAYERS value and the additional coat-specific texture slots and uniform arrays:
三个材质文件涵盖三种涂层数量。所有文件在结构上相同,仅 NB_COAT_LAYERS 值、额外的涂层纹理槽位和 Uniform 数组不同:
| File文件 | NB_COAT_LAYERS | Total textures总纹理数 | Coat normal slots涂层法线槽位 | Extra uniforms额外 Uniform |
|---|---|---|---|---|
| material_multi_layered_pbr.hlsl | 1 | 5 | t5 | CoatInfo01, CoatExtinctionColor01 |
| material_multi_layered_pbr2.hlsl | 2 | 6 | t5, t6 | CoatInfo01-02, CoatExtinctionColor01-02 |
| material_multi_layered_pbr3.hlsl | 3 | 7 | t5, t6, t7 | CoatInfo01-03, CoatExtinctionColor01-03 |
Each CoatInfo float4 packs: (IOR, roughness, thickness, unused). Each CoatExtinctionColor float4 packs (R, G, B, unused) extinction coefficients for Beer-Lambert absorption per coat.
每个 CoatInfo float4 打包:(IOR, roughness, thickness, unused)。每个 CoatExtinctionColor float4 打包每个涂层 Beer-Lambert 吸收的 (R, G, B, unused) 消光系数。
// Array sizes derived from NB_COAT_LAYERS: // NB_TOTAL_NORMALS = NB_COAT_LAYERS + 1 (coat normals + base normal) // NB_TOTAL_LOBES = NB_COAT_LAYERS + 1 (one lobe per normal) // NB_TOTAL_LAYERS = NB_COAT_LAYERS*2+1 (Coat + Medium) per coat + Base // Energy coefficient array tracks what fraction of light reaches each layer float layer_energy_coeff[NB_TOTAL_LAYERS]; // DirectLighting sums all lobes: for (int lobe = 0; lobe < NB_TOTAL_LOBES; lobe++) { light_specular += specular_gd[lobe] * data_rw.fresnel[lobe] * layer_energy_coeff[lobe_to_layer[lobe]]; }
built_in/layered_pbr/ framework files (stack_lit.hlsl, ibl.hlsl, hammersley.hlsl, bsdf/) exist in both directories with largely identical content. The key difference: the new engine adds the three material_multi_layered_pbr*.hlsl driver files in res_upgrade/ that wire the framework into the production material pipeline, making it a first-class material type selectable in the editor (up to 3 coat layers). In res/shader/ the framework existed but had no production material using it.
新旧对比:built_in/layered_pbr/ 框架文件(stack_lit.hlsl、ibl.hlsl、hammersley.hlsl、bsdf/)在两个目录中都存在且内容基本相同。关键区别:新引擎在 res_upgrade/ 中新增了三个 material_multi_layered_pbr*.hlsl 驱动文件,将框架接入生产材质管线,使其成为可在编辑器中选择的一流材质类型(最多 3 个涂层)。res/shader/ 中框架存在但没有生产材质使用。