20 · MeshStandardMaterial 定制:在光照前修改表面参数

返回系列目录

Standard 的优势在于统一的金属度—粗糙度材质模型。定制时优先修改进入 BRDF 的表面参数,让内置灯光、阴影和环境反射继续计算。直接在输出端覆盖颜色无法等价表达金属变粗糙或表面变湿。

参数与视觉的联系

base color 对非金属主要表示漫反射颜色;在金属工作流中还影响金属的反射颜色。metalness 控制金属与非金属的材质行为,roughness 控制微表面分布的粗糙程度。roughness 小,反射通常更集中清晰;它不是透明度或简单亮度。

混合金属度常用于不同材质区域的过渡或覆盖关系。不要为了“更亮”就任意把非金属 metalness 调高;金属缺少合适环境反射时可能显得很暗。验证金属效果需要有信息的环境或光照。

参数在哪形成

r185 Standard 使用 meshphysical.glsl.js 的基础路径。其顺序包括:

#include <map_fragment>
#include <color_fragment>
// alpha 处理省略
#include <roughnessmap_fragment>
#include <metalnessmap_fragment>
// 法线、自发光等处理省略
#include <lights_physical_fragment>
#include <lights_fragment_begin>

roughnessmap_fragment 创建 roughnessFactor,并在存在贴图时乘绿色通道;metalnessmap_fragment 创建 metalnessFactor,并乘蓝色通道。普通 roughnessMap 和 metalnessMap 属于数据贴图,不能设置为 sRGB 颜色贴图。

lights_physical_fragment 会利用参数建立 PhysicalMaterial,并给 roughness 添加与几何法线变化有关的修正及有效下限。在该 Chunk 之前改 roughnessFactor,通常比在之后强行覆盖 material.roughness 更容易保留内置防走样处理。

实例:金属表面的局部氧化覆盖

这个示例用 UV 生成示意遮罩,实际产品可替换为纹理遮罩。几何需要 uv attribute;自行传 vDemoOxideUv,避免依赖只有内置贴图开关启用时才存在的 vUv。

import * as THREE from 'three';
import { replaceOnce } from './patch-utils.js';

export function createOxideMaterial() {
  const uniforms = { uDemoOxideAmount: { value: 0.5 } };
  const material = new THREE.MeshStandardMaterial({
    color: '#aeb6c2', metalness: 1, roughness: 0.22
  });
  material.onBeforeCompile = shader => {
    Object.assign(shader.uniforms, uniforms);
    shader.vertexShader = replaceOnce(shader.vertexShader, '#include <common>',
      '#include <common>\nvarying vec2 vDemoOxideUv;', 'oxide vertex declarations');
    shader.vertexShader = replaceOnce(shader.vertexShader, '#include <uv_vertex>',
      '#include <uv_vertex>\nvDemoOxideUv = uv;', 'oxide uv');
    shader.fragmentShader = replaceOnce(shader.fragmentShader, '#include <common>',
      `#include <common>
       varying vec2 vDemoOxideUv;
       uniform float uDemoOxideAmount;`, 'oxide fragment declarations');
    shader.fragmentShader = replaceOnce(shader.fragmentShader, '#include <color_fragment>',
      `#include <color_fragment>
       float demoPattern = 0.5 + 0.5 * sin(vDemoOxideUv.x * 23.0)
         * sin(vDemoOxideUv.y * 19.0);
       float demoCoverage = clamp(uDemoOxideAmount, 0.0, 1.0);
       float demoOxide = (1.0 - smoothstep(demoCoverage - 0.08,
         demoCoverage + 0.08, demoPattern)) * step(0.0001, demoCoverage);
       diffuseColor.rgb = mix(diffuseColor.rgb, vec3(0.18, 0.055, 0.018), demoOxide);`,
      'oxide base color');
    shader.fragmentShader = replaceOnce(shader.fragmentShader, '#include <roughnessmap_fragment>',
      `#include <roughnessmap_fragment>
       roughnessFactor = mix(roughnessFactor, 0.9, demoOxide);`, 'oxide roughness');
    shader.fragmentShader = replaceOnce(shader.fragmentShader, '#include <metalnessmap_fragment>',
      `#include <metalnessmap_fragment>
       metalnessFactor = mix(metalnessFactor, 0.0, demoOxide);`, 'oxide metalness');
  };
  material.customProgramCacheKey = () => 'demo/oxide/v1';
  return { material, uniforms };
}

这里的线性 RGB 常量是示意颜色,不是十六进制颜色简单除以 255 后的 sRGB 值。要使用美术指定颜色,可用 THREE.Color uniform,遵循第 14 章的颜色管理规则。

同一遮罩控制三个参数:覆盖区变成氧化物颜色、变粗糙、降低金属度。只改 base color 会得到染色金属,而非相同的氧化层观感。此例只模拟表面参数,不增加真实厚度、剥落轮廓或微观几何。

边缘遮罩是连续混合。amount 接近 1 时因软阈值仍可能保留少量过渡;若业务要求精确“全部覆盖”,可在 JavaScript/GLSL 明确端点语义,不要误以为 smoothstep 自动表达产品状态。

保留与替换的选择

示例保留 roughnessmap_fragment,所以原贴图仍影响未覆盖区域,然后按遮罩覆盖到 0.9。若完全删除这个 Chunk,需要自行声明 roughnessFactor,并决定如何处理 roughnessMap、UV 宏及相关功能。一般无需为了一个局部混合承担整段责任。

也不要直接替换整个 lights_physical_pars_fragment 来调粗糙度:那会接管大量 BRDF、环境采样和材质结构,升级成本很高。只有明确需要改变反射模型本身时,才进入该层。

验证与拓展

先在有连续 UV 的球体上测试,再检查模型 UV 接缝。固定相机改变环境方向,确认金属区域与氧化区域的反射不同。随后添加 roughnessMap,验证补丁仍保留原逻辑。高频正弦遮罩在远距离会走样,可使用合适频率、导数过滤或 mipmapped 遮罩纹理;演示公式不等于最终美术纹理方案。

下一步可增加 normalMap 来表现粗糙表面,但“roughness 高”与“法线起伏大”不是同一参数。混合法线需考虑其空间和归一化,见纹理混合实战。

练习(暂不提供答案)

  1. 分别只修改 base color、只修改 roughness 和同时修改三项,记录可观察差异。
  2. 用灰度遮罩纹理替换正弦图案,确保 UV 和颜色空间设置正确。
  3. 将效果改为涂漆金属剥落:覆盖区是非金属油漆,裸露区是金属,说明遮罩方向。
  4. 明确 amount 为 0 和 1 的精确端点行为,并实现无残留的全覆盖与全裸露状态。