26 · 动态变色与设备状态扫描

返回系列目录

设备监控场景常需要“沿模型向上扫描一条亮带”。如果直接覆盖最终输出,亮带会遮掉原来的光照细节。这里把扫描拆成基础颜色和自发光两部分,让模型仍然具有粗糙度、高光和环境响应。

完整运行代码:color-scan.js。它只依赖系列的 runtime 和 patch-utils,无需外部纹理。运行方法见 README。代码使用 TorusKnotGeometry 代替业务设备模型,算法可用于具有 position 的普通 Mesh。

1. 数据从哪里来

JavaScript 创建 uDemoTime: { value: 0 } 和 uDemoScanColor: { value: new THREE.Color(...) },在 onBeforeCompile 中将同一包装对象赋给 shader.uniforms。GLSL fragment shader 声明相同名字和类型。

高度是逐顶点数据,顶点阶段把局部 transformed.y 写入 vDemoHeight;GPU 在三角形内进行透视正确插值,片元阶段就能知道该表面像素的局部高度。此例未叠加变形,因此在 begin_vertex 后读取足够;若还有位移,应决定扫描跟随原始坐标还是变形后的坐标。

// vertex 全局
varying float vDemoHeight;
// begin_vertex 后
vDemoHeight = transformed.y;

局部高度让扫描跟随模型旋转。如果需求是整个厂房统一水平扫描,应改用世界高度,且正确包含对象及实例变换。

2. 从距离构造柔和光带

设当前扫描中心为 c,片元高度为 y,距离为 d=∣y−c∣d=|y-c|。用两个边界把距离映射到权重:

b=1−smoothstep⁡(0.08,0.22,d) b=1-\operatorname{smoothstep}(0.08,0.22,d)

小于 0.08 的核心带权重为 1,大于 0.22 则为 0,中间平滑过渡。它比硬阈值更适合连续扫描,但不是完整的像素抗锯齿方案,极窄的带仍可能闪烁。

// color_fragment 后
float demoCenter = 1.5 * sin(uDemoTime);
float demoBand = 1.0 - smoothstep(0.08, 0.22, abs(vDemoHeight - demoCenter));
diffuseColor.rgb = mix(diffuseColor.rgb, uDemoScanColor, demoBand);

选 color_fragment 后,是为了在内置 map 和顶点色已经作用后修改颜色。如果放在 map_fragment 前,扫描色会继续被贴图相乘;两者不是谁更“正确”,而是不同产品需求。

3. 自发光与最终输出不同

// emissivemap_fragment 后
totalEmissiveRadiance += uDemoScanColor * demoBand * 0.7;

基础颜色进入后续光照计算;自发光作为发出的辐射加入 outgoingLight,不依赖灯照才能可见。这里保留内置 emissiveMap 的结果再叠加扫描,不会把原发光纹理清空。

自发光不会自动照亮附近物体,也不会自动产生屏幕外扩的 bloom。需要 bloom 时再添加后处理,并在 HDR、曝光和色调映射一致的情况下调节强度。直接写 gl_FragColor 则容易绕开这些已建立的流程。

4. 更新与颜色类型

onFrame((dt, elapsed) => {
  state.uDemoTime.value = elapsed;
});
// 外部业务状态变更时:
state.uDemoScanColor.value.set('#ff7043');

不需要 needsUpdate,程序结构没有改变。用 THREE.Color 从 CSS 色值构建工作颜色,避免把 UI 中的 sRGB 数字未经转换直接塞进工作线性空间。若使用 setRGB,要明确传入值所属的色彩空间。

验证与边界

关闭灯光后应仍看到自发光带,但基础色区域明显变暗;把自发光系数设为 0,带内应保留明暗差异;转动 Mesh 后带应随局部坐标旋转。把材质换成金属度 1 时 base color 的物理含义有所变化,不能期待与非金属设备完全相同。

本例给一个材质一套状态。多台设备共享材质会同步扫描;需要独立扫描时可使用独立材质状态,或设计逐实例 attribute,不能每帧简单轮流改同一个值后期待自动记住所有设备状态。

练习

  1. 把往返正弦扫描改为从底部到顶部循环扫描,处理回绕瞬间。
  2. 分别在 map_fragment 前和 color_fragment 后注入,解释带色与贴图的组合差异。
  3. 添加业务状态颜色切换,确保不触发 shader 重编译。
  4. 实现世界空间水平扫描,并验证旋转 Mesh 后效果仍水平。