Day 117 · 着色器实战 — 扫描脉冲、能量发光与顶点动画
Day 116 你学会了 GLSL 基础,写出会呼吸的发光平面。今天把它们拼成真正的工业特效——三种最常见的工业 3D 效果:扫描脉冲(光带沿设备扫过,体现"数据流动")、设备能量发光(选中/告警时设备外圈泛光)、顶点动画(形状变形,如能量柱波动)。每一个都是"Day 116 积木(sin/mix/smoothstep)"的组合应用。做完,你的车间有了"工业数字孪生"的观感——特效不再是贴图,而是 GPU 实时算出来的。
目录
一、工业特效清单与着色器选型
选型原则(Day 116 记忆钩子):颜色/光效 → 片元;形状变形 → 顶点。今天的三个特效两个片元一个顶点——正好把两个阶段都用上。
二、扫描脉冲:smoothstep 光带沿设备扫过
2.1 思路
在片元着色器里,用 uv.y(表面的上下位置,0~1)和 uTime 算出一条"随时间上移的光带":
// 扫描脉冲片元着色器(挂在设备表面的半透明罩上)
uniform float uTime;
uniform vec3 uColor;
varying vec2 vUv;
void main() {
// 1. 光带位置:0~1 之间随时间循环上移
float scanPos = fract(uTime * 0.3); // fract:取小数,0→1 循环
// 2. 光带宽度:当前像素离光带中心越近越亮
// smoothstep(边1, 边2, x) 在 0~1 平滑过渡——宽度的"柔边"
float band = 1.0 - smoothstep(0.0, 0.08, abs(vUv.y - scanPos));
// 3. 光带色 × 亮度,叠加在设备原本颜色上(Additive 思路)
vec3 color = vec3(0.02, 0.05, 0.08); // 设备底色(暗)
color += uColor * band * 0.8; // 光带叠加
gl_FragColor = vec4(color, 1.0);
}
核心就两行:
fract(uTime * 0.3)造"移动位置",smoothstep造"柔边光带"。这就是"扫描"的着色器本质——一个位置 + 一段渐变。
2.2 顶点着色器要传 uv
片元里用 vUv,顶点着色器要把几何的 uv 传过去:
// 顶点着色器(配套)
varying vec2 vUv;
void main() {
vUv = uv; // 把内置 uv 传给片元
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}
varying 是"顶点→片元"的桥梁:几何自带的
uv通过它传到片元。Day 116 没用到 uv,今天它成了扫描定位的坐标。
三、设备能量发光:uTime 驱动外圈泛光
3.1 思路
给设备加一个"半透明发光罩"(稍大的几何),片元里用 sin(uTime) 控制亮暗呼吸:
// 能量发光片元着色器(半透明外罩)
uniform float uTime;
uniform vec3 uColor;
uniform float uState; // 0=正常 / 1=告警(第 6 个月接 WS 时喂数据)
void main() {
// 呼吸强度:0.4~1.0 往复(Day 116 同款)
float breath = 0.7 + 0.3 * sin(uTime * 2.0);
// 状态色:正常用主色,告警用红色(mix 渐变)
vec3 normalColor = uColor;
vec3 alarmColor = vec3(1.0, 0.23, 0.19);
vec3 color = mix(normalColor, alarmColor, uState);
// 外圈边缘更亮(用片元坐标远离中心的程度)——简化:均匀泛光
gl_FragColor = vec4(color * breath, 0.35);
}
3.2 为什么用"外罩"而不是改模型
给设备"套一个半透明罩"(稍大的几何体,法线朝外)
→ 罩的片元 = 纯发光,不干扰模型本体
→ 与模型分开管理:模型管形状,罩管"气场"
工程价值:发光罩是独立 Mesh + 独立 ShaderMaterial——状态变化只改
uStateuniform,不碰模型。这就是"特效与模型解耦":第 6 个月接 WS 时,一行uState.value = 1就让设备进入告警泛光。
四、顶点动画:能量柱的波动
4.1 思路(顶点着色器!)
让圆柱体的"顶面顶点"随 sin 上下起伏——改顶点位置 = 形状变形:
// 能量柱顶点着色器:顶部按 sin(uTime) 波动
uniform float uTime;
varying vec2 vUv;
void main() {
vUv = uv;
// 把位置拷出来改
vec3 pos = position;
// 只有"顶部附近"的顶点才波动(vUv.y 接近 1 = 顶部)
float topFactor = smoothstep(0.85, 1.0, vUv.y);
pos.y += sin(uTime * 3.0 + vUv.x * 6.0) * 0.15 * topFactor;
gl_Position = projectionMatrix * modelViewMatrix * vec4(pos, 1.0);
}
// 用这个顶点着色器创建"波动能量柱"
const energyColumn = new THREE.Mesh(
new THREE.CylinderGeometry(0.3, 0.3, 3, 32, 16), // 细分足够(坑 2)
new THREE.ShaderMaterial({
uniforms: { uTime: { value: 0 } },
vertexShader: `...上边那段...`,
fragmentShader: `...纯色或渐变...`,
})
);
注意细分参数:
CylinderGeometry(..., 32, 16)——段数越多顶点越多,波动越平滑。顶点动画对几何细分敏感(坑 2 会讲)。
五、实战:把特效挂到设备上
把三个特效装进车间引擎(Day 112 引擎扩展):
// src/core/three/attach-fx.ts(特效装配)
import * as THREE from "three";
import { createScanShader, createGlowShader, createWaveVertex } from "./shaders";
/**
* 给设备挂特效:
* ① 扫描罩(半透明,光带扫过)
* ② 发光罩(选中/告警泛光)
* 返回一个 setState 接口:改 uState 驱动颜色(第 6 个月接 WS 用)
*/
export function attachDeviceFx(model: THREE.Object3D): {
setState: (s: "normal" | "alarm") => void;
} {
// 计算模型包围盒,决定罩子尺寸
const box = new THREE.Box3().setFromObject(model);
const size = box.getSize(new THREE.Vector3());
// ① 扫描罩:比模型略大的半透明柱/盒
const scanMat = new THREE.ShaderMaterial({
uniforms: { uTime: { value: 0 }, uColor: { value: new THREE.Color(0x00c6ff) } },
vertexShader: SCAN_VERTEX,
fragmentShader: SCAN_FRAGMENT,
transparent: true,
depthWrite: false,
});
const scanMesh = new THREE.Mesh(new THREE.BoxGeometry(size.x * 1.1, size.y * 1.1, size.z * 1.1), scanMat);
scanMesh.position.y = size.y / 2; // 罩子包住模型
model.add(scanMesh); // 挂到模型子级(跟着动)
return {
setState(s) {
// 第 6 个月接 WS:这里从"改 uniform"变成"读 store 状态"
scanMat.uniforms.uColor.value.setHex(s === "alarm" ? 0xff3b30 : 0x00c6ff);
},
};
}
// —— 循环里:更新所有特效的 uTime ——
// fxMeshes.forEach((m) => { (m.material as THREE.ShaderMaterial).uniforms.uTime.value = clock.getElapsedTime(); });
观察:特效 mesh 挂到
model.add(scanMesh)——模型移动/旋转/缩放,特效跟着走(Group 层级红利,Day 102)。特效与模型的"同步"不需要额外代码。
六、常见坑点
坑 1:片元里用了 vUv 但顶点没传
症状:着色器报错/扫描没效果。修法:顶点着色器必须 vUv = uv 并声明 varying vec2 vUv(第二节配套)。
坑 2:顶点动画"棱角分明"不圆滑
原因:几何段数太少(顶点稀疏)。修法:CylinderGeometry(r, r, h, 32, 16)——提高细分(第四节)。
坑 3:发光罩"穿帮"(罩住模型看不清)
原因:罩子太密/太实。修法:transparent + depthWrite:false + 透明度 0.3 左右;罩子尺寸留 1.05~1.1 余量。
坑 4:特效 uTime 不动(扫描静止)
原因:循环里没更新 uniform。修法:每帧 uniforms.uTime.value = clock.getElapsedTime()(Day 116 第五节)。
坑 5:特效 mesh 没跟着设备走
原因:直接 scene.add(特效) 而不是 model.add(特效)。修法:挂到设备子级(第五节)。
七、自测挑战
T1 · 扫描脉冲(50 分钟)
实现第二节:光带沿设备表面循环上扫。调 smoothstep 的 0.08(带宽)和 uTime*0.3(速度),做出"快扫/慢扫"两种节奏。
T2 · 能量发光(50 分钟)
实现第三节:设备外圈泛光呼吸。给 setState 接一个按钮/按键,切换"正常蓝/告警红"——验证 uState 驱动的状态切换。
T3 · 能量柱波动(40 分钟)
实现第四节:圆柱体顶部波浪起伏。对比段数 4 vs 32 的平滑度差异——把"细分=质量"写进笔记。
T4 · 特效组装(40 分钟)
把三个特效挂到车间一台设备上(扫描 + 泛光 + 波动),确认它们随设备移动、互不干扰。截图对比"有特效 vs 无特效"——这就是 Day 119 验收的视觉证据。
八、总结
车间有了"数字孪生"观感:光在扫、设备在发光、能量柱在波动。明天做本周的收尾功夫——性能预算与动效纪律。
明日预告:Day 118 性能与体验优化——FPS 预算表(每个效果花几 ms)、动效纪律(该省的省)、以及"降档开关"的系统化,把本周所有效果整理成一份可复用的性能预算。