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。本例注入使用内部字段,升级时必须重新核对。
练习(暂不提供答案)
- 将状态带改成由业务进度从底部向上填充,保留柔和边界。
- 只改变自发光,保持基础颜色不变,比较有光与无光环境下的结果。
- 对比 shininess 为 10、40、100 的效果,解释它与 roughness 的语义差别。
- 将局部高度带改为世界空间高度带,并说明物体平移与旋转后应看到什么。