29 · 湿润表面与局部积水

返回系列目录

湿地面常同时表现为颜色加深、微表面更平滑、上层薄膜反光增强。只把颜色调暗可以做出“污渍”,却很难说明湿润的感觉。本章使用 MeshPhysicalMaterial,将同一个湿润遮罩作用于三个物理参数。

完整可运行代码:wet-surfaces.js。示例用球面和方向光让高光更容易观察;遮罩是沿局部 Y 方向投射的圆柱区域,不是水流或积水模拟。地面项目可替换为 XZ 平面及业务遮罩。

1. 先让功能进入编译变体

const material = new THREE.MeshPhysicalMaterial({
  color: '#8c7461',
  roughness: 0.9,
  clearcoat: 1,
  clearcoatRoughness: 0.08
});
const state = { uDemoWetness: { value: 0 } };

即使当前湿度为 0,也把 clearcoat 保持为 1,以便编译出 USE_CLEARCOAT 路径;实际每片元强度由补丁乘以遮罩。若把基础 clearcoat 设为 0,相关代码可能根本不进入程序,这时仅修改自定义 uniform 无法启用被预处理删除的功能。

这样做会让干燥区域也承担该程序变体的计算成本,适合连续过渡。完全不同且长期干燥的物体可以使用更简单的材质,但要权衡变体数量。

2. 构造湿度遮罩

顶点把 transformed 传给 vDemoLocal,片元计算:

float demoWet = (1.0 - smoothstep(0.7, 2.0, length(vDemoLocal.xz))) * uDemoWetness;

圆心附近湿度高,外侧逐渐变干;uDemoWetness 在 [0,1] 范围内控制整体程度。变成真实积水时可换成高度图、绘制遮罩或雨水模拟结果。这些输入属于线性数据,不应标记为 sRGB。

3. 三个位置分别修改什么

// color_fragment 后
diffuseColor.rgb *= mix(1.0, 0.55, demoWet);

// roughnessmap_fragment 后
roughnessFactor = mix(roughnessFactor, 0.18, demoWet);

// lights_physical_fragment 后、lights_fragment_begin 前
#ifdef USE_CLEARCOAT
  material.clearcoat *= demoWet;
#endif

前两处改变进入光照模型的 base color 与粗糙度。第三处必须放在 lights_physical_fragment 后,因为局部 PhysicalMaterial material 在该 Chunk 内才声明并填充;同时必须位于光照累积之前,否则清漆参数已被消费,改了也不影响已算出的光。

保留原 Chunk 还保留了 clearcoat 贴图、粗糙度下限与几何粗糙度处理。不要为了一个遮罩把整个 PhysicalMaterial 初始化代码复制到自己的补丁里,这会增加升级维护负担。

4. 参数组合的含义

base roughness 描述底层表面,clearcoatRoughness 描述上层清漆近似。它们不是同一参数的重复设置。降低 roughness 会使高光更集中,不保证所有视角下都更亮;方向光、环境和视角共同决定结果。

不能用增加 metalness 代替湿润。湿石头仍是介电体,设置成金属会改变反射和漫反射的物理模型。clearcoat 是薄上层的近似,也不等于具有可见厚度、折射和波纹的水洼。

示例省略环境纹理,方向光足以比较高光变化。写实项目应准备合适环境光照,避免看到“没有反射内容”就误判低粗糙度无效。

5. 动画与验证

onFrame((dt, elapsed) => {
  state.uDemoWetness.value = 0.5 + 0.5 * Math.sin(elapsed * 0.7);
});

暂停在湿度 0 和 1,分别关闭颜色、粗糙度和 clearcoat 修改,比较三个因素的贡献。验证光照仍存在、透明度没有变化、程序数量不随时间增长。改动湿区半径只需 uniform;把“是否安装清漆补丁”做成代码选项则要进入 cache key。

练习

  1. 将圆形湿区改为一条逐渐扩大的拖地痕迹。
  2. 比较仅降低 base roughness、仅增加 clearcoat 和两者同时变化的结果。
  3. 加入线性遮罩贴图,并让湿区跟随世界坐标而非模型旋转。
  4. 解释本例为何不能模拟透明水洼的折射,并列出升级所需数据。