站内搜索

输入关键词开始搜索

Cesium 体积火焰:光线步进、噪声与点线面绘制

文章目录

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

城市上空的体积火焰绘制场景与点线面、噪声和质量参数面板

实现思路导图

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 层数和屏幕覆盖面积。

相关文章

Cesium FFT 矩形水面:GPU 频谱海浪与随波船体

在 Cesium Primitive 中接入多级联 FFT 海浪,完成位移、法线、清澈度、LOD 网格和船体姿态采样

Cesium 无人机管道航道:Tube 几何与菲涅尔流光材质

从航点平滑、管道网格构建到半透明空心材质、边缘辉光、状态配色和场景拾取