21 · MeshPhysicalMaterial 定制:清漆、透射与扩展层
MeshPhysicalMaterial 在 Standard 基础上增加更多物理外观特性。r185 的两者共享 meshphysical.glsl.js,Physical 通过宏与参数开启附加路径。不能因为文件名含 physical,就认为 Standard 可以直接访问全部 clearcoat 或 transmission 字段。
优先使用已有属性
| 特性 | 日常用途 | 重要区别 |
|---|---|---|
| clearcoat / clearcoatRoughness | 车漆、涂层、湿润表层 | 额外反射层,不等同于降低底层 roughness |
| transmission / thickness | 玻璃、透明树脂等透射外观 | 与 opacity 透明混合不是同一模型 |
| ior | 折射率及非金属反射关系 | 改变光学响应,不是简单扭曲强度 |
| attenuationColor / attenuationDistance | 有色体积的吸收近似 | 与厚度和距离相关,不是单纯涂色 |
| sheen | 织物边缘的柔和光泽 | 与清漆的锐利表层不同 |
| iridescence | 薄膜干涉色 | 颜色依赖角度及薄膜参数 |
| anisotropy | 拉丝金属等方向性高光 | 需要正确切线方向和适合的法线数据 |
| dispersion | 透射色散 | 增加采样/计算成本,需结合 transmission |
如果贴图属性已经支持所需遮罩,优先使用 clearcoatMap、transmissionMap 等,不需要补丁。程序化空间规则、多输入联动或业务驱动才是自定义注入的理由。
Chunk 之外也有关键代码
lights_physical_fragment 建立 material 的清漆、薄膜等字段;transmission_fragment 修改漫反射贡献以加入透射;最终 outgoingLight 附近还有位于 ShaderLib 主体中的 sheen 与 clearcoat 累积代码。
所以不能假设有一个可在顶层直接替换的 #include <clearcoat_fragment> 就能接管全部清漆逻辑。真实锚点要以 r185 源码为准。若修改已经算好的 clearcoat 结构体字段,应该在 lights_physical_fragment 后、灯光累积前,而不是等光照算完后才改参数。
实例:随水位变化的局部清漆层
下面用模型局部高度定义湿润区域。几何局部 y 位于 -1~1 时容易观察;清漆功能先用非零 clearcoat 开启,再由自定义遮罩控制局部覆盖。
import * as THREE from 'three';
import { replaceOnce } from './patch-utils.js';
export function createWetCoatMaterial() {
const uniforms = { uDemoWetLevel: { value: 0 } };
const material = new THREE.MeshPhysicalMaterial({
color: '#b98360',
metalness: 0,
roughness: 0.65,
clearcoat: 1,
clearcoatRoughness: 0.12
});
material.onBeforeCompile = shader => {
Object.assign(shader.uniforms, uniforms);
shader.vertexShader = replaceOnce(shader.vertexShader, '#include <common>',
'#include <common>\nvarying float vDemoWetHeight;', 'wet vertex declarations');
shader.vertexShader = replaceOnce(shader.vertexShader, '#include <begin_vertex>',
'#include <begin_vertex>\nvDemoWetHeight = transformed.y;', 'wet height');
shader.fragmentShader = replaceOnce(shader.fragmentShader, '#include <common>',
`#include <common>
varying float vDemoWetHeight;
uniform float uDemoWetLevel;`, 'wet fragment declarations');
shader.fragmentShader = replaceOnce(shader.fragmentShader, '#include <color_fragment>',
`#include <color_fragment>
float demoWetMask = 1.0 - smoothstep(uDemoWetLevel - 0.06,
uDemoWetLevel + 0.06, vDemoWetHeight);
diffuseColor.rgb *= mix(1.0, 0.72, demoWetMask);`, 'wet base color');
shader.fragmentShader = replaceOnce(shader.fragmentShader, '#include <roughnessmap_fragment>',
`#include <roughnessmap_fragment>
roughnessFactor = mix(roughnessFactor, 0.3, demoWetMask);`, 'wet roughness');
shader.fragmentShader = replaceOnce(shader.fragmentShader, '#include <lights_physical_fragment>',
`#include <lights_physical_fragment>
#ifdef USE_CLEARCOAT
material.clearcoat *= demoWetMask;
#endif`, 'wet coat');
};
material.customProgramCacheKey = () => 'demo/wet-coat/v1';
return { material, uniforms };
}
将 level 从 -1.2 增加到 1.2,可观察从干燥到整体湿润的过程。降低底层 roughness 和添加清漆层表现的是不同贡献;先单独启用两者再同时启用,有助于理解。这里不是物理流体模拟,只是受高度控制的表面外观近似。
修改 material.clearcoat 的局部字段不会改变内置全局 uniform,也不会破坏其他片元。示例在内置 Chunk 后乘遮罩,保留了 clearcoatMap 等原有调制及清漆粗糙度修正。若要按片元改变 clearcoatRoughness,应考虑保留内部几何粗糙度和下限处理,不要直接写死零。
为什么把 clearcoat 设为 1,再局部乘零
USE_CLEARCOAT 决定相关结构体字段和计算是否存在。如果 JavaScript clearcoat 为 0,程序可能根本不编译该功能;在注入中引用清漆字段就会失败,或被宏保护整段跳过。想在运行时通过 uniform 连续出现和消失,就应保持功能启用,让遮罩控制其贡献。
代价是即使遮罩全为零,程序仍包含清漆路径。少量对象连续动画通常可以接受;大量长期关闭的对象可以使用另一结构变体。不要为了每帧阈值变化反复开关功能并触发编译。
transmission 的特别限制
透射不仅是一段颜色公式,还涉及渲染器准备透射用的场景图像。只在 GLSL 中硬加宏或字段,不能确保渲染器完成所需的前置渲染。应通过 material.transmission 等 API 开启功能,再在支持的阶段调制局部参数。
玻璃通常用 transmission 表达透射,并保持适当 opacity;将 opacity 降低主要是混合背景,并不能替代折射和厚度吸收。透明排序、屏幕空间可见信息、背景和物体重叠会影响结果,不能承诺得到光线追踪级别的多层玻璃。
定制顶点位置时,透射还依赖与变形一致的世界位置、法线和投影;只修改 gl_Position 会让光学计算仍使用旧位置。需遵循第 22~25 章的空间与配套规则。
验证与源码核对
使用有明暗结构的环境贴图和可移动的方向光观察清漆反射。分别测试遮罩全开、全关、过渡区和非均匀缩放。Physical 的额外功能可能增加程序和渲染成本,只启用实际需要的功能。
核对 MeshPhysicalMaterial.js 的属性 setter、ShaderLib.js 的 Standard/Physical uniforms 组合,以及 meshphysical.glsl.js、lights_physical_fragment.glsl.js、transmission_fragment.glsl.js。功能开关、uniform 参数和片元局部字段是三个不同层次,修改其中一层不会自动补齐另外两层。
练习(暂不提供答案)
- 将湿润遮罩改成世界高度,比较物体旋转后水线的行为。
- 分别只降低 roughness、只增加 clearcoat、同时使用两者,记录反射变化。
- 把 clearcoat 设为 0,检查宏保护下的结果,解释为什么 uniform 遮罩无法创建未编译的功能。
- 设计车漆局部划痕:划痕处清漆变弱、底层参数改变,说明两个层的参数应该在哪修改。