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 参数和片元局部字段是三个不同层次,修改其中一层不会自动补齐另外两层。

练习(暂不提供答案)

  1. 将湿润遮罩改成世界高度,比较物体旋转后水线的行为。
  2. 分别只降低 roughness、只增加 clearcoat、同时使用两者,记录反射变化。
  3. 把 clearcoat 设为 0,检查宏保护下的结果,解释为什么 uniform 遮罩无法创建未编译的功能。
  4. 设计车漆局部划痕:划痕处清漆变弱、底层参数改变,说明两个层的参数应该在哪修改。