# fire

体积火焰插件，基于 Three.js 官方 `webgpu_volume_fire` 示例封装。插件在 GPU 上运行 3D 流体模拟，并通过独立的 volumetric pass 叠加到 `Viewer.renderPipeline` output effect 链中，提供火焰、烟雾、扰动、阴影和动态点光源效果。

```typescript
import { Vector3 } from 'three/webgpu';
import { FireEffect } from 'u-space/plugins/fire';

const fire = new FireEffect(viewer, {
  position: new Vector3(0, 0, 0),
  size: new Vector3(12, 12, 24),
  fireIntensity: 40,
  smokeLifespan: 3.5,
});

fire.enable();
```

## 能力

- 按官方示例的 semi-Lagrangian advection、curl noise、buoyancy、Jacobi pressure projection 流程模拟体积火焰和烟雾。
- 使用 3D storage texture 保存速度、密度、温度、压力和 curl noise 数据。
- 默认使用 `TeapotGeometry(0.8, 28)` 作为 emitter 采样几何；也可以传入业务对象和自定义 `BufferGeometry`。
- 通过 `RenderPipeline.addOutputEffect()` 叠加体积结果，不占用业务侧的 `setOutputComposer()`。
- 内置 spot light 和 point light：spot light 负责体积阴影方向，point light 跟随火焰核心投射暖色动态光照。
- 监听 `cameraChange`，相机对象切换后会重建 fire 自己的 volumetric pass。

## API

### `new FireEffect(viewer, options?)`

创建火焰效果实例。构造函数不会立即往场景写入模拟资源；调用 `enable()` 后才会创建体积盒、storage texture、compute pass、灯光和 output effect。

### `enable(options?)`

开启火焰效果。重复调用时等同于 `update(options)`。启用期间会把 `viewer.frameloop` 临时切到 `always`，关闭时恢复启用前的值。

```typescript
fire.enable({
  position: new Vector3(0, 0, 0),
  size: new Vector3(12, 12, 24),
  renderResolution: 0.5,
});
```

### `update(options?)`

更新火焰参数。大多数视觉和模拟强度参数会通过 uniform 直接生效。`gridSize`、`emitterGeometry`、`volumeLayer` 或 `raymarchSteps` 改变时会重建相关资源。

```typescript
fire.update({
  emitTemperature: 6,
  emitDensity: 8,
  emitterRadius: 1.4,
  smokeLifespan: 6,
});
```

### `setPosition(position)`

更新体积盒底部中心位置。等同于 `update({ position })`。

### `setEmitter(emitter, geometry?)`

设置驱动 emitter 的对象和采样几何。`emitter` 的 `matrixWorld` 会在每帧写入模拟；如果只想使用默认内部 emitter，可以传入 `null`。

### `reset()`

清空当前模拟状态并重新创建模拟资源。

### `disable()`

关闭火焰效果，移除 `beforeRender` / `cameraChange` 监听、output effect、体积盒、灯光、storage texture 和 compute pass，并恢复启用前的 `viewer.frameloop`。

### `dispose()`

关闭效果并释放插件持有的默认 emitter geometry。

## Options

| 参数 | 类型 | 默认值 | 说明 |
| --- | --- | --- | --- |
| `position` | `Vector3` | `(0, 0, 0)` | 体积盒底部中心世界坐标 |
| `size` | `Vector3` | `(12, 12, 24)` | 体积盒世界尺寸，也是烟雾扩散的约束范围 |
| `gridSize` | `{ x?: number; y?: number; z?: number }` | `{ x: 100, y: 100, z: 200 }` | 3D 模拟网格尺寸，越大越细腻也越耗显存 |
| `volumeLayer` | `number` | `10` | 体积盒所在 layer，用于隔离 volumetric pass |
| `emitterGeometry` | `BufferGeometry \| null` | `TeapotGeometry(0.8, 28)` | emit pass 采样的几何顶点 |
| `emitter` | `Object3D \| null` | 内部 `Object3D` | 驱动 emitter matrix 和扰动速度的对象 |
| `emitterRadius` | `number` | `1` | emitter 移动时影响流场的半径，作为 uniform 更新 |
| `simulate` | `boolean` | `true` | 是否推进流体模拟 |
| `simSpeed` | `number` | `1.2` | 模拟速度倍率 |
| `pressureIterations` | `number` | `2` | Jacobi pressure projection 次数，会自动向上限制为正偶数 |
| `raymarchSteps` | `number` | `16` | 体积 raymarch 步数，改变后会重建体积材质 |
| `renderResolution` | `number` | `0.5` | volumetric pass 分辨率倍率，范围 `0.1..1` |
| `denoise` | `boolean` | `true` | 是否对体积 pass 做 Gaussian blur |
| `denoiseStrength` | `number` | `0.5` | 降噪强度 |
| `bloom` | `boolean` | `true` | 是否对火焰输出追加 bloom |
| `bloomStrength` | `number` | `0.1` | bloom 强度 |
| `bloomRadius` | `number` | `1` | bloom 半径 |
| `bloomThreshold` | `number` | `0.5` | bloom 阈值 |
| `smokeLifespan` | `number` | `3.5` | 烟雾寿命，越大烟雾保留越久 |
| `smokeIntensity` | `number` | `1` | 烟雾散射可见度倍率 |
| `smokeColor` | `ColorRepresentation` | `0xffffff` | 烟雾散射颜色 |
| `fireLifespan` | `number` | `1.3` | 火焰温度衰减时间 |
| `turbulence` | `number` | `3.2` | 火焰和烟雾湍流强度 |
| `turbulenceDecay` | `number` | `0.1` | 湍流随年龄衰减速度 |
| `turbulenceFrequency` | `number` | `10` | curl noise 频率 |
| `buoyancy` | `number` | `3` | 热浮力 |
| `velocityDamping` | `number` | `0.25` | 速度阻尼 |
| `emitDensity` | `number` | `7` | emitter 注入密度 |
| `emitTemperature` | `number` | `5.5` | emitter 注入温度 |
| `motionBoost` | `number` | `0.25` | emitter 移动带来的额外发射量 |
| `windStrength` | `number` | `6.5` | emitter 移动对流场的扰动强度 |
| `fireIntensity` | `number` | `40` | 火焰发光强度 |
| `glowSpread` | `number` | `5` | 火焰亮度扩散范围 |
| `fireHue` | `number` | `0` | 火焰色相偏移，单位为度 |
| `saturation` | `number` | `1.1` | 输出饱和度 |
| `fireStartColor` | `ColorRepresentation` | `0xffe68c` | 高温火焰颜色 |
| `fireMidColor` | `ColorRepresentation` | `0xff7305` | 中段火焰颜色 |
| `fireEndColor` | `ColorRepresentation` | `0xff0000` | 低温火焰颜色 |
| `phaseAsymmetry` | `number` | `0` | Henyey-Greenstein 相函数 g 值 |
| `powderStrength` | `number` | `0.59` | powder effect 强度 |
| `multiScattering` | `number` | `1` | 多次散射近似强度 |
| `shadowAbsorption` | `number` | `2` | 体积阴影吸收系数 |
| `shadowAmbient` | `number` | `0.5` | 阴影环境补光 |
| `keyLight` | `boolean` | `true` | 是否创建体积阴影 spot light |
| `pointLight` | `boolean` | `true` | 是否创建火焰动态点光源 |
| `pointLightIntensity` | `number` | `1` | 动态点光源强度 |
| `pointLightDistance` | `number` | `40` | 动态点光源距离 |

## Emitter

如果不设置 `emitterGeometry`，插件会创建一个内部 `TeapotGeometry(0.8, 28)`，这和官方示例保持一致。emit pass 会遍历该几何的 position 顶点，把密度和温度写入体积纹理。传入自定义 `emitterGeometry` 时，该几何必须包含 `position` attribute。

如果不设置 `emitter`，插件会使用内部 `Object3D`，并把它放在 `position` 位置。传入业务对象后，插件每帧读取该对象的 `matrixWorld` 和世界位置变化，移动速度会影响流场扰动和额外发射量。

```typescript
const fire = new FireEffect(viewer, {
  emitter: torchMesh,
  emitterGeometry: torchFlameGeometry,
  emitterRadius: 0.8,
});
```

## 光源与体积范围

火焰本体不是场景里的普通透明 mesh 直接混合，而是先放在 `volumeLayer` 的体积盒里单独渲染，再通过 output effect 叠加到主场景。烟雾和火焰的可见范围由 `position + size` 定义的体积盒限制，超出盒子的密度会在边界处淡出。要让烟雾扩散范围更大，优先增大 `size`；要提升细节，再同步增大 `gridSize`。

插件会按需创建两个光源：

- `FireEffect.keyLight`：`SpotLight`，用于体积阴影方向和烟雾散射计算。
- `FireEffect.pointLight`：`PointLight`，跟随 emitter / 火焰核心移动，并通过 TSL `colorNode` 产生随温度、密度和噪声变化的暖色照明。

如果业务场景已经有自己的灯光，可以通过 `keyLight: false` 或 `pointLight: false` 关闭插件内置光源。

## 与 RenderPipeline 的关系

`FireEffect` 使用 `viewer.renderPipeline.addOutputEffect()` 和 `removeOutputEffect()` 接入输出链。它不会调用 `setOutputComposer()`，因此不会覆盖业务或其他插件的 output effects。注意如果业务主动设置自定义 `setOutputComposer()`，RenderPipeline 会按自定义 composer 的语义跳过 output effects，火焰叠加也会被跳过。

`renderResolution` 只影响独立 volumetric pass 的分辨率；主场景分辨率仍由 `Viewer` 和 renderer 控制。启用 `denoise` 和 `bloom` 时，插件会在 output effect 内创建并释放对应节点。

## 示例

示例页见 `examples/test_fire.html`。本地运行时先构建插件：

```bash
pnpm build:plugins -- plugins/fire
```

然后通过本地静态服务访问：

```text
http://127.0.0.1:5174/examples/test_fire.html
```
