19 · MeshPhongMaterial 定制:基础色、高光与自发光

返回系列目录

MeshPhongMaterial 使用 Blinn–Phong 类光照模型。它适合需要直接控制高光颜色和锐利程度的效果。相比 Standard,它不使用金属度—粗糙度的物理参数模型;不要把 shininess 当成 roughness 的同名替代。

先判断是否需要补丁

整个物体统一变色可更新 material.color;统一高光颜色可更新 material.specular;统一高光锐利程度可更新 material.shininess;整体自发光可更新 material.emissive 和 emissiveIntensity。这些都不必注入 GLSL。

需要自定义的典型条件是:参数按程序化空间规则变化、由多个输入共同决定、只作用于表面的某个区域。例如设备外壳上流动的状态带,既改变区域颜色又使其发光,外壳其他区域继续接受普通光照。

光照计算的三个入口

r185 meshphong.glsl.js 中先构造 diffuseColor 和 totalEmissiveRadiance,再处理贴图、法线和自发光,随后进入 lights_phong_fragment。该 Chunk 创建的结构体字段为:

BlinnPhongMaterial material;
material.diffuseColor = diffuseColor.rgb;
material.specularColor = specular;
material.specularShininess = shininess;
material.specularStrength = specularStrength;

这里的 material 是片元局部 GLSL 结构体,不是 JavaScript 材质实例。

想改变什么 合适位置 原因
接受光照的基础色 color_fragment 后 保留 map 和顶点色,随后进入 diffuse 光照
自发光颜色 emissivemap_fragment 后 totalEmissiveRadiance 最终直接加入光照和,不依赖普通漫反射
高光局部参数 lights_phong_fragment 后、灯光累积前 BlinnPhongMaterial 已初始化,可修改字段
最终整幅表面的调色 opaque_fragment 前修改 outgoingLight 影响已累计的光照;需保留后续 tone mapping 和输出转换

实例:设备局部坐标中的移动状态带

以下函数假定普通静态 Mesh,不处理蒙皮后的空间定义;几何顶点局部 y 大致位于 -1~1。返回 time uniform,供现有动画循环更新。

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

export function createPhongStatusMaterial() {
  const uniforms = { uDemoStatusTime: { value: 0 } };
  const material = new THREE.MeshPhongMaterial({
    color: '#b7bdc8',
    specular: '#eeeeee',
    shininess: 40
  });

  material.onBeforeCompile = shader => {
    Object.assign(shader.uniforms, uniforms);
    shader.vertexShader = replaceOnce(shader.vertexShader, '#include <common>',
      '#include <common>\nvarying float vDemoStatusHeight;', 'status vertex declaration');
    shader.vertexShader = replaceOnce(shader.vertexShader, '#include <begin_vertex>',
      '#include <begin_vertex>\nvDemoStatusHeight = transformed.y;', 'status height');
    shader.fragmentShader = replaceOnce(shader.fragmentShader, '#include <common>',
      `#include <common>
       uniform float uDemoStatusTime;
       varying float vDemoStatusHeight;`, 'status fragment declaration');
    shader.fragmentShader = replaceOnce(shader.fragmentShader, '#include <color_fragment>',
      `#include <color_fragment>
       float demoCenter = sin(uDemoStatusTime) * 0.8;
       float demoBand = 1.0 - smoothstep(0.06, 0.18,
         abs(vDemoStatusHeight - demoCenter));
       vec3 demoStatusColor = vec3(0.05, 0.65, 0.3);
       diffuseColor.rgb = mix(diffuseColor.rgb, demoStatusColor, demoBand);`,
      'status diffuse');
    shader.fragmentShader = replaceOnce(shader.fragmentShader, '#include <emissivemap_fragment>',
      `#include <emissivemap_fragment>
       totalEmissiveRadiance += demoStatusColor * demoBand * 0.8;`, 'status emissive');
    shader.fragmentShader = replaceOnce(shader.fragmentShader, '#include <lights_phong_fragment>',
      `#include <lights_phong_fragment>
       material.specularShininess = mix(material.specularShininess, 90.0, demoBand);`,
      'status shininess');
  };
  material.customProgramCacheKey = () => 'demo/phong-status/v1';
  return { material, uniforms };
}

片元中的高度来自顶点 varying 插值。对于直边三角形,局部 y 在表面线性变化;无需为状态带单独增加大量顶点,因为带状计算在片元进行。若改成顶点位移,几何细分才直接影响轮廓平滑度。

增大 smoothstep 的两个边界间距,边缘会更软;增大 shininess,常见视觉是高光更集中,但不是线性的“亮度滑杆”。移动灯光才能观察高光变化。没有灯光时仍可看到自发光带,但普通外壳不会凭空获得漫反射照明。自发光只改变自身输出,不会自动照亮附近对象,也不会自动产生屏幕泛光。

不应修改 uniform 声明本身

不能在 GLSL 写 shininess = 90.0;,因为 uniform 是只读输入。改局部 material.specularShininess 才能按片元调整。也不能直接把 Phong Chunk 拷贝到 Standard,因为两个结构体和 BRDF 函数依赖不同。

示例按原始局部高度固定图案。物体移动后条带跟随模型移动;如果要求条带固定在世界某个高度,需要传世界坐标并明确实例化和蒙皮位置,不能只把变量改名为 worldHeight。

验证与限制

在普通球体或盒子上加入一个方向光和弱环境光,旋转模型、移动光源,检查带状颜色、发光和高光是否一起按预期变化。补丁没有移动顶点或裁切表面,因此不需要为它修改阴影几何。

源码依据是 ShaderLib/meshphong.glsl.js、ShaderChunk/lights_phong_fragment.glsl.js 与 lights_phong_pars_fragment.glsl.js。本例注入使用内部字段,升级时必须重新核对。

练习(暂不提供答案)

  1. 将状态带改成由业务进度从底部向上填充,保留柔和边界。
  2. 只改变自发光,保持基础颜色不变,比较有光与无光环境下的结果。
  3. 对比 shininess 为 10、40、100 的效果,解释它与 roughness 的语义差别。
  4. 将局部高度带改为世界空间高度带,并说明物体平移与旋转后应看到什么。