文章目录
体积火焰案例允许在地图上绘制点、线和面。每个结果不是一张面向相机的固定贴图,而是在包围体内执行光线步进,用三维噪声计算火焰密度和颜色。

实现思路导图
flowchart TD
ROOT[体积火焰]
ROOT --> SHAPE[空间形状]
SHAPE --> DRAW[绘制点线面]
SHAPE --> ENU[ENU 局部包围盒]
ROOT --> VOLUME[体积积分]
VOLUME --> RAY[射线与盒求交]
VOLUME --> MARCH[光线步进]
ROOT --> DENSITY[火焰密度]
DENSITY --> PROFILE[基础轮廓]
DENSITY --> FBM[FBM 风与扰动]
ROOT --> EFFECT[颜色火花光照与质量]
截图中已经绘制一个点火焰和一段线火焰,并把尺度提高到 500 米以便从当前城市视角辨认。左侧轮廓表示用户输入范围,明亮的动态体积才是 Shader 计算结果;这张图展示的是运行状态,不是等待绘制的初始地图。
绘制结果转成火焰实例
DrawTool 返回点位置或线面顶点。页面按类型创建 VolumetricFire 或 VolumetricFireRegion:
if (result.type === "point") {
fire = new VolumetricFire(viewer, options);
} else {
fire = new VolumetricFireRegion(viewer, {
positions: result.positions,
shapeType: result.type,
...options,
});
}
线和面还保留轻量轮廓 Entity,帮助用户辨认原始绘制范围。
绘制坐标不能直接塞进 Shader。实现先取点或顶点的中心,在该位置建立 ENU 局部坐标系,再把经纬度顶点变换到局部米制平面。火焰包围盒、线宽、风向和噪声都在这个小坐标系中计算,最后通过模型矩阵放回 Cesium 世界坐标。
光线步进构造体积感
片元 Shader 求相机射线与火焰包围体的进入、离开距离,再沿射线多次采样密度。采样结果从后向前或前向后累积颜色与透明度。
密度由位置、时间、风向和多层 Simplex 噪声共同决定。底部密度高,顶部逐渐收窄,并让噪声域随时间向上移动,形成火焰舔舐感。
for (int i = 0; i < MAX_STEPS; i++) {
float density = sampleFire(rayPosition, time);
color += transmittance * density * fireColor;
transmittance *= 1.0 - density;
}
具体过程是:先把相机位置乘逆模型矩阵得到局部射线原点,再计算射线与单位包围盒的进入和离开距离。只有命中盒子的像素才步进。每一步用归一化高度、轮廓距离和 FBM 噪声得到密度,再按前向 alpha 合成:
C(next) = C + T · α · fireColor
T(next) = T · (1 - α)
当透射率 T 已很低或射线走出包围盒时可以提前结束。质量档位主要改变最大步数;步数越少越容易出现分层,步数越多则片元成本近似线性增加。
火焰外形不是噪声本身。点模式先用随高度收窄的二维轮廓定义基础密度,噪声只扭曲轮廓;若完全用噪声阈值,会得到漂浮云团而不是从地面生长的火焰。lacunarity 控制每层噪声频率放大,gain 控制振幅衰减,iterations 决定叠加层数。
参数中哪些需要重建
颜色、强度、速度、噪声和迭代参数可以推送给已有实例。缩放会改变包围体,火花和动态光照会改变附属对象,因此案例选择重新创建所有火焰。
页面在重建前保存实例类型与位置,销毁旧资源后按当前参数恢复。这样不会把旧 Primitive、粒子系统或 Light 留在场景中。
参数实际上分三类:
| 类型 | 参数 | 处理方式 |
|---|---|---|
| 每帧 uniform | 时间、颜色、亮度、风、速度、噪声 | 更新已有 Primitive |
| 几何边界 | 尺度、线宽、区域范围 | 更新包围体或重建 |
| 附属系统 | 火花、动态光照开关 | 创建或销毁附属对象 |
把所有滑块都做成重建会造成拖动卡顿;把包围体尺寸只当 uniform,又会让 Cesium 使用旧包围球进行错误剔除。
点、线、面如何复用
点火焰使用一个局部包围体。线和面先在区域内生成多个采样或实例位置,再共享材质和参数。GPU 实例化降低了大量小火焰的提交开销。
线模式在 Shader 中计算采样点到各线段的最短距离,再与半宽比较;面模式使用奇偶规则判断采样点是否位于多边形内部。距离场边缘叠加多尺度噪声后,火焰能沿整条线或整个面连续燃烧,而不是把许多点火焰简单堆成一排球形体。
区域边界传给 Shader 的点数有上限。复杂多边形需要先简化,或改用距离场纹理;否则 uniform 数量和循环次数都会过高。这是当前点线面方案的实际边界。
自动质量
FirePerformanceMonitor 统计帧率和实例数量。开启自动质量后,根据持续性能区间调整步进质量,而不是看到一帧下降就立即切档。频繁在质量档位之间跳动会产生明显画面闪变。
自动质量需要迟滞:持续低于下阈值才降档,持续高于更高的恢复阈值才升档,并设置最短切换间隔。否则 59/60 FPS 附近的波动会让最大步数反复变化,表现为火焰边缘不断跳动。
火花使用轻量粒子补充高频亮点,动态光照只近似影响周围对象,它们不参与体积积分。体积火焰负责主体,附属效果负责可读性;把所有细节都塞进光线步进会让每个覆盖像素承担过多计算。
生命周期
页面每帧只推进 u_time、更新相机局部位置并请求重绘。清空或离开页面时,要按顺序移除轮廓 Entity,销毁火焰 Primitive、粒子系统和光源,再解除 preRender/动画监听。否则地图上虽然看不到火焰,旧回调仍会继续消耗 CPU。
这个案例的主线是:交互绘制确定空间范围 → 包围体内光线步进 → 噪声生成密度 → 火花和光照补充细节 → 性能监控控制质量。
从零复现:体积火焰教程
案例源码位于 src/demos/cesiumdemos/volumetric-fire/,主要实现位于 lib/VolumetricFirePrimitive.js。先完成单点火焰,再扩展线和面。
第 1 步:创建点火焰包围盒
用户点击后取得世界位置,在该点建立 ENU 模型矩阵。创建一个单位 BoxGeometry,通过模型矩阵按火焰宽度和高度缩放。
包围盒只是片元 Shader 的计算范围,最终颜色由内部密度决定。先用半透明线框显示盒子,确认底部贴地、Z 轴向上。
第 2 步:把相机射线转换到局部空间
每帧把相机世界位置乘包围盒逆模型矩阵,得到 u_camLocalPos。片元顶点位置同样使用局部坐标,射线方向为两者之差。
vec3 rayOrigin = u_camLocalPos;
vec3 rayDirection = normalize(v_localPosition - rayOrigin);
射线与单位盒求交得到 tNear/tFar。未命中、交点在相机后方或路径长度为零时立即丢弃。
第 3 步:定义没有噪声的基础火焰
先按归一化高度 h 构造随高度收窄的轮廓,底部宽、顶部尖,并用一张纵向火焰梯度纹理控制颜色和基础密度。
完成标志:关闭时间和噪声后,应看到稳定的锥形或水滴形火焰。如果这一步像一团球,后续 FBM 只会得到抖动的球。
第 4 步:加入 FBM 扰动和风
对局部位置做多层 Simplex 噪声:每层频率乘 lacunarity,振幅乘 gain。时间沿高度轴移动噪声域,水平风位移随高度增大。
noisePosition.xy += u_windDirection * h;
noisePosition.z -= u_time * u_speed;
float displacedHeight = h + sqrt(h) * u_magnitude * fbm(noisePosition);
噪声采样次数和光线步数是相乘关系。4 层 FBM × 48 步意味着一个覆盖像素最多执行 192 次噪声层计算。
第 5 步:沿射线累积颜色
把 [tNear, tFar] 划分为 u_maxSteps。每步取密度并执行前向 alpha 合成,透射率足够低时提前结束。
accumulatedColor += transmittance * alpha * fireColor;
transmittance *= 1.0 - alpha;
if (transmittance < 0.02) break;
先用固定步长调试。若出现明显切片,再增加步数或加入每像素抖动起点,而不是只提高透明度掩盖。
第 6 步:扩展到线和多边形
将绘制顶点转到同一 ENU 平面。线模式计算采样点到各线段最短距离;面模式用奇偶规则判断点是否在多边形内部。该形状函数乘基础火焰密度。
形状点数超过 uniform 上限时先简化边界,或把距离场预生成到纹理。复杂多边形不能无限展开为 Shader 循环。
第 7 步:增加火花与动态光照
火花使用独立 ParticleSystem 或轻量屏幕粒子,动态光照使用火焰中心附近的近似点光源。它们读取同一个颜色、强度和时间参数,但不进入光线积分。
截图中的点火焰和线火焰来自两个空间形状,500 米尺度是演示参数;实际业务应根据地图比例和相机高度设置。
第 8 步:实现自动质量
统计滑动窗口 FPS 和帧时间。持续低于下阈值时减少步数或 FBM 层数;持续高于恢复阈值时升档,并设置最短切换间隔。
销毁时同时移除 Primitive、轮廓 Entity、火花、光源和 preRender 监听。重新创建参数前先保存空间形状,再按新配置恢复。
验收与排错
- 从某些角度火焰消失:检查射线盒求交和包围球。
- 火焰像滚动贴图:基础轮廓与噪声应使用三维局部位置。
- 线火焰断成多段:检查点到线段距离与线宽单位。
- GPU 时间过高:同时降低光线步数、FBM 层数和屏幕覆盖面积。