# Flowing Light 2D Pass (流光 2D 通道)

- **Summary**: `FlowingLight2DPass` 提供屏幕空间的流光(光带)特效,使用 InstancedMesh 实例化渲染优化性能,沿着 2D 归一化坐标路径绘制动态光流,适用于突出导航路径、指示线或动态路径演示。
- **Schema**: `FlowingLight2DPass` 类及 Line 数据结构。
- **Concepts**: 后处理通道、屏幕空间光带、InstancedMesh 实例化渲染、归一化坐标系统、头尾追逐动画。
- **Configuration**: 归一化路径点、颜色、不透明度、线宽、动画时长、延迟。
- **Examples**: 基础集成、多条路径、动态更新、高 DPR 场景优化。

## Schema

> **Definition**: [FlowingLight2DPass](../../five/renderer/postprocessing/passes/flowing-light-2d-pass.d.ts)

`FlowingLight2DPass` 是 `FivePass` 的子类,核心接口如下:

```ts
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;             // 动画时长(毫秒)
  startTime?: number;            // 线条开始时间(毫秒),默认使用当前内部时间
  lineWidth?: number;            // 线宽(归一化单位)
  tailLengthRatio?: number;      // 尾巴长度比例(0-1),默认 0.2
};

export class FlowingLight2DPass extends FivePass {
  constructor(camera: Camera);

  // 批量设置路径列表(替换所有现有线条)
  public setLines(lines: Line[]): void;

  // 追加一条或多条线,返回生成的 id 数组
  public addLine(...lines: Line[]): string[];

  // 按 id 移除一条或多条线,返回实际移除的数量
  public removeLine(...ids: string[]): number;

  // 修改单条线的参数(通过 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;
}
```
## Concepts

### InstancedMesh 实例化渲染 (Instanced Rendering)
为了优化高 DPR(Device Pixel Ratio)场景下的性能,该 Pass 使用 InstancedMesh 代替全屏着色器。每个线段作为一个实例,只渲染线段覆盖的区域,避免了对每个像素的全屏检查,显著提升了渲染效率。

### 归一化坐标系统 (Normalized Coordinates)
路径点使用归一化坐标 `[0, 1]` 定义:
- `(0, 0)` 表示屏幕左上角
- `(1, 1)` 表示屏幕右下角
- `(0.5, 0.5)` 表示屏幕中心

这种坐标系统与屏幕分辨率无关,自动适配不同尺寸和 DPR。

### 头尾追逐动画 (Head-Tail Chase Animation)
流光效果采用"头尾追逐"模式:
- **头部(Head)**: 光流的前端,沿路径循环移动
- **尾部(Tail)**: 光流的后端,跟随头部移动
- **渐变**: 从尾部(透明度 0)到头部(透明度 1)线性渐变
- **尾长**: 尾部长度由 `tailLengthRatio` 参数控制(默认 0.2,即路径总长的 20%)

光头在路径上循环流动,通过像素空间投影计算每个像素的渐变位置,确保渐变效果准确。尾巴长度会在动画开始时逐渐增长,直到达到 `tailLengthRatio * totalLength`。

### NDC 坐标转换
内部实现中,归一化坐标 `[0, 1]` 会转换为 NDC(Normalized Device Coordinates) `[-1, 1]`,与 WebGL 标准坐标系统一致,确保投影计算准确。

### DPR 处理与宽高比适配
自动处理高 DPR 场景和不同屏幕宽高比:
- **DPR 补偿**: 通过 `uPixelRatio` uniform 自动补偿高 DPR 设备的光线宽度,确保在 Retina 等高分辨率屏幕上视觉一致
- **像素空间计算**: 所有距离计算在像素空间进行,而非 NDC 空间,确保横向和纵向光线宽度在任何宽高比下都保持一致
- **各向同性渲染**: 无论屏幕是横屏、竖屏还是方形,光线的扩散效果在各个方向上都保持相同

## Configuration

### Line 配置参数

| 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` | 动画周期(毫秒)。光头从路径起点完成一个周期的时长。 |
| `startTime` | `number` | | 当前内部时间 | 线条开始时间(毫秒)。控制线条动画从何时开始播放,可用于多条线错开起始时间。 |
| `lineWidth` | `number` | | `0.01` | 线宽(归一化单位)。控制光带在屏幕上的宽度。 |
| `tailLengthRatio` | `number` | | `0.2` | 尾巴长度比例(0-1)。控制流光尾巴相对于路径总长的比例。 |

> **注意**: `totalLength` 和 `lineWidth` 使用归一化单位,与屏幕实际像素无关。

## Instance Methods

### `constructor(camera: Camera)`
初始化 Pass,需传入相机以获取屏幕分辨率。

### `setLines(lines: Line[])`
批量设置路径列表,替换所有现有线条。可随时调用以修改或新增路径。

**自动生成 ID**: 如果 line 没有提供 `id`,会自动生成 UUID。
**自动设置 startTime**: 如果 line 没有提供 `startTime`,会使用当前内部时间。

**示例**:
```ts
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,
  startTime: 500,
  lineWidth: 0.01,
};
pass.setLines([pathLine]);
```

### `addLine(...lines: Line[]): string[]`
追加一条或多条线到现有列表中,不影响已有线条。返回新增线条的 id 数组。

**示例**:
```ts
// 追加单条线
const [id1] = pass.addLine({
  points: [
    new THREE.Vector2(0.1, 0.1),
    new THREE.Vector2(0.9, 0.9),
  ],
  totalLength: 1.13,
  color: new THREE.Color(0xff0000),
  duration: 2000,
});

// 追加多条线
const ids = pass.addLine(
  {
    points: [...],
    totalLength: 0.8,
    color: new THREE.Color(0x00ff00),
    duration: 1500,
  },
  {
    points: [...],
    totalLength: 1.2,
    color: new THREE.Color(0x0000ff),
    duration: 1800,
  },
);
// ids = ['uuid-1', 'uuid-2']
```

### `removeLine(...ids: string[]): number`
按 id 移除一条或多条线,返回实际移除的数量。如果 id 不存在,会在控制台输出警告。

**示例**:
```ts
// 移除单条线
pass.removeLine(id1);

// 移除多条线
const removedCount = pass.removeLine(id1, id2, id3);
console.log(`移除了 ${removedCount} 条线`);
```

### `setLine(params: { id: string } & Partial<Omit<Line, 'id'>>)`
通过 `id` 修改单条线的参数。支持部分更新,只更新传入的字段。

**性能优化**:
- 如果 `points` 数量不变,只更新 GPU 的 instance attributes,性能极高
- 如果 `points` 数量改变,会重建整个 mesh,性能开销较大

**示例**:
```ts
// 只修改颜色 - 高性能
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` 时同时更新 `totalLength`

### `render(...)`
由 EffectComposer 自动调用,无需手动调用。自动同步渲染目标尺寸。

### `dispose()`
释放 InstancedMesh、着色材质和渲染目标资源,避免内存泄漏。销毁 Pass 时必须调用。

## Examples

### 快速上手 (Quick Example)

```typescript
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();
```


### 多条路径 & 动态更新

```typescript
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,
    startTime: 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]);
});
```


### 计算路径总长度

```typescript
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,
};
```


### 矩形路径示例

```ts
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,
  startTime: 2000,
  lineWidth: 0.01,
};

pass.setLines([rectPath]);
```

### 高 DPR 场景优化

```typescript
// 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,
  startTime: i * 200,
  lineWidth: 0.01,
}));

pass.setLines(paths);
```


## Debugging

- **光带不出现**: 检查 `color` 和 `opacity`,确保非全透明;验证 `points` 至少 2 个;确认坐标在 [0, 1] 范围内。
- **流速不对**: 检查 `totalLength` 计算是否准确;若流速过快/过慢,调整 `duration`。
- **位置偏移**: 确认坐标系统使用归一化坐标 [0, 1],不是像素坐标;注意坐标系统以左上角为原点,Y 轴方向为 0(上) 到 1(下)。
- **性能问题**: InstancedMesh 实现已优化,但过多路径点仍会影响性能。建议单条路径不超过 20 个点。

## Performance

### 与全屏着色器对比
- **全屏着色器**: 每帧检查所有像素,DPR=2 时像素数增加 4 倍,性能下降明显
- **InstancedMesh**: 仅渲染线段覆盖区域,DPR=2 时性能影响较小
- **实测**: 在 DPR=2 场景下,InstancedMesh 方案 FPS 提升约 3-5 倍

### setLine 性能优化
`setLine` 方法根据修改类型自动选择最优更新策略:

| 修改类型 | 性能 | 说明 |
|---------|------|------|
| 只修改颜色/透明度 | ⚡️ 极快 (~100x) | 只更新 instanceColor attribute |
| 只修改 duration/startTime | ⚡️ 极快 (~100x) | 只更新 instanceMeta attribute |
| 只修改 lineWidth | ⚡️ 极快 (~100x) | 只更新 instanceData attribute |
| 修改 points (相同数量) | ⚡️ 快 (~50x) | 只更新 instanceStart/End attributes |
| 修改 points (不同数量) | ⚠️ 慢 | 重建整个 mesh |

**最佳实践**:
```ts
// ✅ 高性能 - 频繁修改颜色
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);
```

### 优化建议
1. 减少路径点数量,使用更简化的路径
2. 降低 `lineWidth`,减少渲染区域
3. 控制同时渲染的路径数量
4. 使用 `setLine` 而非 `setLines` 来更新单条线
5. 保持 points 数量不变,只修改坐标
6. 在低端设备上可通过 `pass.enabled = false` 禁用

## Common Pitfalls

1. **坐标系统混淆**: 必须使用归一化坐标 [0, 1],不能使用像素坐标。坐标系统以左上角为原点,Y 轴方向为 0(上) 到 1(下)。
2. **totalLength 计算错误**: 光流速度直接依赖 `totalLength`。手动计算时务必精确,使用归一化单位。
3. **lineWidth 过大**: 归一化单位下,0.01 约为屏幕高度的 1%。过大的值会导致光带过宽。
4. **未及时 dispose()**: 移除 Pass 时必须调用 `dispose()`,否则 GPU 资源泄漏。
5. **DPR 不匹配**: 确保渲染目标尺寸与实际屏幕 DPR 匹配,Pass 会自动处理。

## Related

*   [Postprocessing](./postprocessing.md): 后处理通用概念与 EffectComposer 使用指南。
*   [Flowing Light 3D Pass](./flowing-light-3d-pass.md): 基于 3D 世界坐标的流光效果。

---


tags: [流光, 光带, 导航路径, 指示线, 动态路径, 特效, postprocessing, effect, flowing, light, pass, rendering, screenspace, instanced, performance, 2d, screen-space]
