42 · Roughness、Metalness 与 AO:让纹理混合保持材质一致性

返回系列目录

完整运行案例:csm-pbr-blend.js。它生成两张线性 DataTexture,不依赖网络图片;程序化遮罩同步混合颜色、粗糙度和金属度,演示金属表面的锈蚀过渡。文件内变量 paint 是蓝色表面样本的名字,演示中把它视为带色裸金属;真实不透明油漆是非金属覆盖层,不能照抄其 metalness 值。

1. csm_Roughness 覆盖的是已采样 factor

CSM 6.4.0 默认 csm_Roughness = roughness,实际补丁为:

#include <roughnessmap_fragment>
roughnessFactor = csm_Roughness;

原来的 chunk 先计算 roughness × roughnessMap.g,CSM 随后把这个结果覆盖。因此写 csm_Roughness *= 0.5 只是把材质 roughness 减半,不是把已经乘上 roughnessMap 的值减半。

若需要保留内置贴图:

void main() {
  #ifdef USE_ROUGHNESSMAP
    csm_Roughness *= texture2D(roughnessMap, vRoughnessMapUv).g;
  #endif
  csm_Roughness = clamp(csm_Roughness * uDemoRoughnessScale, 0.0, 1.0);
}

这会在源码中保留内置采样又覆盖其结果,编译器可能消除无用计算,但不要把优化效果视为保证。完全程序化时不需要放一张“占位 roughnessMap”;直接输出需要的数值即可。Uniform 更新不改变 shader 结构。

后面 lights_physical_fragment 仍设置最小粗糙度、加入 geometryRoughness 并限制上界,所以输出 0 不一定得到理想数学镜面。粗糙度控制微表面方向分布,而不是简单“把高光变暗”。对照实验应保持灯光、颜色和相机相同。

2. csm_Metalness 具有相同的覆盖语义

#include <metalnessmap_fragment>
metalnessFactor = csm_Metalness;

默认是 material.metalness;内置 metalnessMap 使用 B 通道,但其结果会被覆盖。需要时自行乘 texture2D(metalnessMap, vMetalnessMapUv).b。金属度 0 表示非金属,1 表示金属;过渡值适合纹理边界覆盖率或艺术化混合,不应把普通塑料设成 0.5 来“增强高光”。

Standard/Physical 根据 metalness 同时改变漫反射贡献和镜面反射颜色:

Cdiffuse contribution=Cbase(1−m) C_{\text{diffuse contribution}}=C_{\text{base}}(1-m)

因此金属在没有适当照明/环境时可能显得很暗。示例用直接灯光展示差异;生产商品展示通常还需要合理的环境照明,不能用提升 emissive 掩盖缺少环境的问题。

3. csm_AO 的 0 与常见 AO 贴图恰好相反

它默认是 0。补丁保留内置 AO,然后追加:

#include <aomap_fragment>
reflectedLight.indirectDiffuse *= 1.0 - csm_AO;

所以 csm_AO 表示额外遮蔽量:0 不遮蔽,1 完全去除间接漫反射。常规 AO 纹理白色通常表示无遮挡,若数据为可见度 visibility,应写 csm_AO = 1.0 - visibility。

它不覆盖内置 aoMap,二者会叠加;也不重做内置 AO 对间接镜面项等分量的完整处理。这个 CSM 补丁只改 indirectDiffuse,不直接压暗直射光、高光或自发光。在纯金属表面,漫反射本就接近零,因此效果可能几乎看不见。

csm_AO = clamp(cavityMask * uDemoOcclusion, 0.0, 1.0);

这是凹槽的近似环境遮蔽,不是几何阴影,也不能检测动态遮挡。示例仅用锈蚀遮罩演示数值通路,真实资产应从几何或烘焙数据获取合理 cavity/AO,而不是认为所有锈斑都处于阴影中。

4. 实践:同一遮罩驱动一组表面属性

设遮罩 w∈[0,1]w\in[0,1],两种表面的属性分别为 A 和 B:

C=(1−w)CA+wCB,r=(1−w)rA+wrB,m=(1−w)mA+wmB C=(1-w)C_A+wC_B,\quad r=(1-w)r_A+wr_B,\quad m=(1-w)m_A+wm_B
vec3 surfaceA = texture2D(uDemoPaint, vDemoUv * 4.0).rgb;
vec3 surfaceB = texture2D(uDemoRust, vDemoUv * 4.0).rgb;
csm_DiffuseColor = vec4(mix(surfaceA, surfaceB, mask), 1.0);
csm_Roughness = mix(0.24, 0.92, mask);
csm_Metalness = mix(0.8, 0.0, mask);

只混颜色时锈斑仍拥有原来光滑金属的高光,视觉上像印刷图案;同步改变粗糙度和金属度,才表达表面材料变化。mask 的过渡宽度避免硬阈值闪烁,但过窄仍可能混叠。

示例 DataTexture 的 RGB 字节由线性数值生成,并标记 LinearSRGBColorSpace。若替换成外部 PNG/JPEG 的基础色照片,通常应标记 SRGBColorSpace;roughness、metalness、mask 保持 NoColorSpace。不要把颜色和数值纹理都设置成 sRGB,也不要对已经正确解码的采样再手写 pow。

增加 normal 纹理时,还必须考虑切线空间、重新归一化和混合方法。两个编码在 [0,1] 的 RGB 直接混合不是通用的法线叠加算法;同一 UV 坐标框架下的表面过渡与细节法线叠加也不是同一种操作。

5. 性能与资源

案例每片元两次纹理采样和少量三角函数;不应把这种示范直接扩展成十几层高频纹理。共用遮罩,按实际设备观察成本。两张纹理放在 uniforms 中,runtime 的 dispose 会收集它们;每帧只更新覆盖阈值,不重新创建材质或纹理。

6. 练习(不附答案)

  1. 给材质设置 roughnessMap,验证只写 csm_Roughness *= 0.5 为什么没有保留贴图变化,再显式恢复 G 通道采样。
  2. 把蓝色区域改成真正的油漆覆盖层,为它选择合理金属度,并说明与裸金属的区别。
  3. 暂时移除 HemisphereLight,只保留 DirectionalLight,解释 AO 效果为何减弱或消失。
  4. 用 RGBA 数据纹理打包遮罩、粗糙度和金属度,设计通道约定并减少纹理采样次数。