【Three.js】day117-shader-practice

作者:mario 发布时间: 2026-09-07 阅读量:6 评论数:0

Day 117 · 着色器实战 — 扫描脉冲、能量发光与顶点动画

Day 116 你学会了 GLSL 基础,写出会呼吸的发光平面。今天把它们拼成真正的工业特效——三种最常见的工业 3D 效果:扫描脉冲(光带沿设备扫过,体现"数据流动")、设备能量发光(选中/告警时设备外圈泛光)、顶点动画(形状变形,如能量柱波动)。每一个都是"Day 116 积木(sin/mix/smoothstep)"的组合应用。做完,你的车间有了"工业数字孪生"的观感——特效不再是贴图,而是 GPU 实时算出来的。


目录


一、工业特效清单与着色器选型

特效

实现思路

用哪个阶段

积木

扫描脉冲

光带位置随时间移动

片元

uTime + smoothstep

能量发光

外圈亮度随状态/时间变化

片元

uTime + sin + mix

能量柱波动

顶点高度随 sin 起伏

顶点

uTime + sin(改 position)

选型原则(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——状态变化只改 uState uniform,不碰模型。这就是"特效与模型解耦":第 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 验收的视觉证据


八、总结

环节

要点

选型

颜色/光效 → 片元;形状变形 → 顶点

扫描脉冲

fract(uTime*速度) 定位 + smoothstep 柔边

能量发光

独立外罩 + uState uniform 驱动状态色

顶点动画

顶点着色器改 position + 高细分几何

解耦

特效独立 Mesh,状态只改 uniform——接 WS 零改动

跟随

特效挂 model.add(),随设备自动同步

车间有了"数字孪生"观感:光在扫、设备在发光、能量柱在波动。明天做本周的收尾功夫——性能预算与动效纪律。


明日预告:Day 118 性能与体验优化——FPS 预算表(每个效果花几 ms)、动效纪律(该省的省)、以及"降档开关"的系统化,把本周所有效果整理成一份可复用的性能预算。

评论