FlowingLight2DPass 提供屏幕空间的流光(光带)特效,使用 InstancedMesh 实例化渲染优化性能,沿着 2D 归一化坐标路径绘制动态光流,适用于突出导航路径、指示线或动态路径演示。FlowingLight2DPass 类及 Line 数据结构。Definition: FlowingLight2DPass
FlowingLight2DPass 是 FivePass 的子类,核心接口如下:
import * as THREE from 'three';
import { FivePass } from './pass';
import { type Camera } from '../../../core/camera';
type Line = {
id?: string; // 可选的唯一标识符,不传则自动生成 UUID
points: THREE.Vector2[]; // 归一化坐标路径点 [0, 1]
totalLength: number; // 路径总长度(归一化单位)
color: THREE.Color; // 光带颜色
opacity?: number; // 光带不透明度(0-1)
duration?: number; // 动画时长(毫秒)
delay?: number; // 动画延迟(毫秒)
lineWidth?: number; // 线宽(归一化单位)
tailLengthRatio?: number; // 尾巴长度比例(0-1),默认 0.2
};
export class FlowingLight2DPass extends FivePass {
constructor(camera: Camera);
// 设置要渲染的路径列表
public setLines(lines: Line[]): void;
// 修改单条线的参数(通过 id)
public setLine(params: { id: string } & Partial<Omit<Line, 'id'>>): void;
public render(
renderer: THREE.WebGLRenderer,
writeBuffer: THREE.WebGLRenderTarget,
readBuffer: THREE.WebGLRenderTarget,
deltaTime: number,
maskActive?: boolean,
): void;
public dispose(): void;
}
为了优化高 DPR(Device Pixel Ratio)场景下的性能,该 Pass 使用 InstancedMesh 代替全屏着色器。每个线段作为一个实例,只渲染线段覆盖的区域,避免了对每个像素的全屏检查,显著提升了渲染效率。
路径点使用归一化坐标 [0, 1] 定义:
(0, 0) 表示屏幕左上角(1, 1) 表示屏幕右下角(0.5, 0.5) 表示屏幕中心这种坐标系统与屏幕分辨率无关,自动适配不同尺寸和 DPR。
流光效果采用"头尾追逐"模式:
tailLengthRatio 参数控制(默认 0.2,即路径总长的 20%)光头在路径上循环流动,通过像素空间投影计算每个像素的渐变位置,确保渐变效果准确。尾巴长度会在动画开始时逐渐增长,直到达到 tailLengthRatio * totalLength。
内部实现中,归一化坐标 [0, 1] 会转换为 NDC(Normalized Device Coordinates) [-1, 1],与 WebGL 标准坐标系统一致,确保投影计算准确。
自动处理高 DPR 场景和不同屏幕宽高比:
uPixelRatio uniform 自动补偿高 DPR 设备的光线宽度,确保在 Retina 等高分辨率屏幕上视觉一致| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
id |
string |
自动生成 UUID | 唯一标识符,用于 setLine 方法更新单条线。 |
|
points |
THREE.Vector2[] |
✓ | - | 归一化坐标路径点数组 [0, 1]。至少 2 个点。 |
totalLength |
number |
✓ | - | 路径总长度(归一化单位)。通常为所有相邻点间距离之和。 |
color |
THREE.Color |
✓ | - | 光带颜色(RGB)。 |
opacity |
number |
1.0 |
光带不透明度(0-1)。控制与背景的混合强度。 | |
duration |
number |
1000 |
动画周期(毫秒)。光头从路径起点完成一个周期的时长。 | |
delay |
number |
0 |
动画延迟(毫秒)。延迟后才开始播放动画。 | |
lineWidth |
number |
0.01 |
线宽(归一化单位)。控制光带在屏幕上的宽度。 | |
tailLengthRatio |
number |
0.2 |
尾巴长度比例(0-1)。控制流光尾巴相对于路径总长的比例。 |
注意:
totalLength和lineWidth使用归一化单位,与屏幕实际像素无关。
constructor(camera: Camera)初始化 Pass,需传入相机以获取屏幕分辨率。
setLines(lines: Line[])更新要渲染的路径列表。可随时调用以修改或新增路径。
自动生成 ID: 如果 line 没有提供 id,会自动生成 UUID。
示例:
const pathLine: Line = {
id: 'my-line-1', // 可选,不传则自动生成
points: [
new THREE.Vector2(0.3, 0.7),
new THREE.Vector2(0.7, 0.7),
new THREE.Vector2(0.7, 0.3),
],
totalLength: 0.8, // 归一化单位
color: new THREE.Color(0x00c2ff),
opacity: 0.8,
duration: 2000,
delay: 500,
lineWidth: 0.01,
};
pass.setLines([pathLine]);
setLine(params: { id: string } & Partial<Omit<Line, 'id'>>)通过 id 修改单条线的参数。支持部分更新,只更新传入的字段。
性能优化:
points 数量不变,只更新 GPU 的 instance attributes,性能极高points 数量改变,会重建整个 mesh,性能开销较大示例:
// 只修改颜色 - 高性能
pass.setLine({
id: 'my-line-1',
color: new THREE.Color(0xff0000),
});
// 修改多个属性 - 高性能
pass.setLine({
id: 'my-line-1',
color: new THREE.Color(0x00ff00),
opacity: 0.5,
duration: 3000,
});
// 修改 points (相同数量) - 高性能
pass.setLine({
id: 'my-line-1',
points: [
new THREE.Vector2(0.2, 0.2),
new THREE.Vector2(0.8, 0.8),
new THREE.Vector2(0.8, 0.2),
], // 仍是 3 个点
totalLength: 1.2,
});
// 修改 points (不同数量) - 会重建 mesh
pass.setLine({
id: 'my-line-1',
points: [
new THREE.Vector2(0.2, 0.2),
new THREE.Vector2(0.8, 0.8),
], // 从 3 个点变为 2 个点
totalLength: 0.85,
});
注意事项:
id 不存在,会在控制台输出警告uTime)不会被重置,流光继续从当前位置播放points 时同时更新 totalLengthrender(...)由 EffectComposer 自动调用,无需手动调用。自动同步渲染目标尺寸。
dispose()释放 InstancedMesh、着色材质和渲染目标资源,避免内存泄漏。销毁 Pass 时必须调用。
import { Five } from '@realsee/five';
import { FlowingLight2DPass } from '../../lib/five/renderer/postprocessing';
import * as THREE from 'three';
const five = new Five();
// 创建 2D 流光通道
const flowing2D = new FlowingLight2DPass(five.camera);
// 定义一条屏幕空间的路径(归一化坐标)
const path = [
new THREE.Vector2(0.2, 0.2),
new THREE.Vector2(0.5, 0.8),
new THREE.Vector2(0.8, 0.2),
];
const totalLen = 1.2; // 归一化单位
flowing2D.setLines([{
points: path,
totalLength: totalLen,
color: new THREE.Color(0x00ff00),
opacity: 0.9,
duration: 2000,
lineWidth: 0.01,
}]);
five.addPass(flowing2D);
function animate() {
requestAnimationFrame(animate);
five.render();
}
animate();
const pass = new FlowingLight2DPass(five.camera);
// 初始路径集合
const paths = [
{
points: [
new THREE.Vector2(0.1, 0.1),
new THREE.Vector2(0.9, 0.1),
],
totalLength: 0.8,
color: new THREE.Color(0xff6600),
duration: 1500,
lineWidth: 0.01,
},
{
points: [
new THREE.Vector2(0.1, 0.5),
new THREE.Vector2(0.9, 0.5),
],
totalLength: 0.8,
color: new THREE.Color(0x0066ff),
opacity: 0.7,
duration: 2000,
delay: 500,
lineWidth: 0.015,
},
];
pass.setLines(paths);
five.addPass(pass);
// 响应用户交互动态更新路径
document.addEventListener('click', (e) => {
const rect = five.canvas.getBoundingClientRect();
const x = (e.clientX - rect.left) / rect.width;
const y = (e.clientY - rect.top) / rect.height; // 左上角为原点,无需翻转
const newPath = {
points: [
new THREE.Vector2(0.5, 0.5),
new THREE.Vector2(x, y),
],
totalLength: Math.hypot(x - 0.5, y - 0.5),
color: new THREE.Color(Math.random() * 0xffffff),
duration: 1200,
lineWidth: 0.01,
};
pass.setLines([...paths, newPath]);
});
function calculatePathLength(points: THREE.Vector2[]): number {
let length = 0;
for (let i = 1; i < points.length; i++) {
length += points[i].distanceTo(points[i - 1]);
}
return length;
}
const points = [
new THREE.Vector2(0.3, 0.7),
new THREE.Vector2(0.7, 0.7),
new THREE.Vector2(0.7, 0.3),
new THREE.Vector2(0.3, 0.3),
new THREE.Vector2(0.3, 0.7),
];
const pathLine = {
points: points,
totalLength: calculatePathLength(points),
color: new THREE.Color(0x00ffff),
duration: 2000,
lineWidth: 0.01,
};
const pass = new FlowingLight2DPass(five.camera);
// 创建一个矩形路径(注意:左上角为原点)
const rectPath = {
points: [
new THREE.Vector2(0.3, 0.3), // 左上
new THREE.Vector2(0.7, 0.3), // 右上
new THREE.Vector2(0.7, 0.7), // 右下
new THREE.Vector2(0.3, 0.7), // 左下
new THREE.Vector2(0.3, 0.3), // 闭合路径
],
totalLength: 1.6, // 矩形周长
color: new THREE.Color(1.0, 1.0, 1.0),
opacity: 1.0,
duration: 4000,
delay: 2000,
lineWidth: 0.01,
};
pass.setLines([rectPath]);
// InstancedMesh 实现自动优化高 DPR 性能
// 在 DPR=2 的 Retina 屏幕上,性能显著优于全屏着色器方案
const pass = new FlowingLight2DPass(camera);
// 多条路径在高 DPR 下仍能保持流畅
const paths = Array.from({ length: 10 }, (_, i) => ({
points: [
new THREE.Vector2(0.1, 0.1 + i * 0.08),
new THREE.Vector2(0.9, 0.1 + i * 0.08),
],
totalLength: 0.8,
color: new THREE.Color(Math.random() * 0xffffff),
duration: 2000 + i * 100,
delay: i * 200,
lineWidth: 0.01,
}));
pass.setLines(paths);
color 和 opacity,确保非全透明;验证 points 至少 2 个;确认坐标在 [0, 1] 范围内。totalLength 计算是否准确;若流速过快/过慢,调整 duration。setLine 方法根据修改类型自动选择最优更新策略:
| 修改类型 | 性能 | 说明 |
|---|---|---|
| 只修改颜色/透明度 | ⚡️ 极快 (~100x) | 只更新 instanceColor attribute |
| 只修改 duration/delay | ⚡️ 极快 (~100x) | 只更新 instanceMeta attribute |
| 只修改 lineWidth | ⚡️ 极快 (~100x) | 只更新 instanceData attribute |
| 修改 points (相同数量) | ⚡️ 快 (~50x) | 只更新 instanceStart/End attributes |
| 修改 points (不同数量) | ⚠️ 慢 | 重建整个 mesh |
最佳实践:
// ✅ 高性能 - 频繁修改颜色
requestAnimationFrame(() => {
pass.setLine({
id: lineId,
color: new THREE.Color(Math.random(), Math.random(), Math.random())
});
});
// ✅ 高性能 - 动画路径(保持点数)
function animatePath(t: number) {
pass.setLine({
id: lineId,
points: [
new THREE.Vector2(0.1, 0.1),
new THREE.Vector2(0.5 + Math.sin(t) * 0.2, 0.5),
new THREE.Vector2(0.9, 0.9),
] // 始终 3 个点
});
}
// ⚠️ 低性能 - 避免频繁改变点数
setInterval(() => {
const pointCount = Math.floor(Math.random() * 5) + 2;
pass.setLine({
id: lineId,
points: generateRandomPoints(pointCount) // 点数不固定
});
}, 100);
lineWidth,减少渲染区域setLine 而非 setLines 来更新单条线pass.enabled = false 禁用totalLength。手动计算时务必精确,使用归一化单位。dispose(),否则 GPU 资源泄漏。tags: [流光, 光带, 导航路径, 指示线, 动态路径, 特效, postprocessing, effect, flowing, light, pass, rendering, screenspace, instanced, performance, 2d, screen-space]