站内搜索

输入关键词开始搜索

Cesium 三维风场:用 WebGL 2 GPU 粒子描绘全球气流

文章目录

这个案例把全球风场变成一组贴着地球运动的发光粒子。粒子的位置、速度和生命周期都在 GPU 中更新,CPU 只负责参数、相机和资源生命周期。

全球三维风场运行效果,地球表面覆盖按风速着色的粒子轨迹

实现思路导图

flowchart TD
  ROOT[全球风场]
  ROOT --> INPUT[数据准备]
  INPUT --> JSON[风场 JSON]
  INPUT --> UV[解析经纬度与 U/V]
  ROOT --> GPU[GPU 粒子计算]
  GPU --> TEX[风速纹理双线性采样]
  GPU --> RK[RK2 推进与 Ping-Pong]
  ROOT --> RENDER[轨迹渲染]
  RENDER --> TRAIL[轨迹衰减]
  RENDER --> CESIUM[Cesium Primitive 合成]
  ROOT --> CONTROL[参数与性能统计]

页面只负责组织状态

WindFieldPage.vue 没有塞入渲染算法。页面把 Cesium 容器交给 useWind3D,再把组合式函数返回的状态传给控制面板:

const {
  activeTheme,
  controls,
  particleCount,
  flyTo,
  selectTheme,
  setParticleCount,
  updateOption,
} = useWind3D(cesiumContainer);

这样划分后,Vue 负责交互,Wind3D 负责 WebGL 资源。切换主题和修改速度因子不需要重建页面。

GPU 粒子的更新链

一帧可以拆成四步:

  1. 从风场纹理读取当前经纬度对应的风速。
  2. 按速度因子推进粒子位置。
  3. 对越界、速度过低或寿命结束的粒子重新播种。
  4. 把上一帧位置和当前帧位置渲染成带方向的轨迹。

粒子状态适合放在浮点纹理中。一次计算会同时更新大量像素,每个像素就是一个粒子。选择 128K、256K 或 512K 时,变化的是参与计算的状态数量,而不是 Vue 节点数量。

颜色和几何表达不同信息

箭头长度、尾迹长度和风场厚度决定空间形态;颜色范围负责把风速映射到色带。截图中低速区域偏蓝,高速区域逐渐过渡到暖色。

速度剔除上下限和颜色映射上下限不是一回事:前者决定粒子是否显示,后者只决定已经显示的粒子用什么颜色。把两组参数混在一起,会出现“调颜色却丢粒子”的问题。

参数更新分成两类

速度、透明度、发光强度等参数可以直接更新 uniform。粒子数量变化会改变状态纹理尺寸,需要重建 GPU 资源:

function setParticleCount(count) {
  if (!wind.value) return;
  wind.value.setParticleCount(count);
  particleCount.value = wind.value.particlesCount;
}

重建期间应暂停旧计算,并在新资源就绪后再恢复渲染。否则同一帧可能同时引用新旧纹理。

性能边界

性能主要受粒子数量、轨迹长度、屏幕覆盖面积和设备填充率影响。移动端优先降低粒子数和尾迹长度。销毁页面时还要解除 preRender 监听、删除 Primitive、纹理和 framebuffer;只销毁 Vue 组件不会自动释放这些 WebGL 对象。

风场数据怎样进入 GPU

风场通常以规则经纬网保存 u、v 两个分量。上传前需要同时记录经纬度范围、格点间隔、最小最大风速和缺测值。Shader 拿到粒子经纬度后,先换算成纹理 UV,再对相邻四个格点做双线性插值。

经纬度位置 → 风场纹理 UV → 读取四邻域 → 插值得到 u/v → 推进粒子

案例使用全球模拟数据,因此经度方向要首尾连续。粒子越过 180° 经线时应回绕,纬度越过数据范围或采到无效风速时则重新播种。若只对经度做 clamp,日期变更线会出现明显空带。

粒子状态为什么用两张纹理

WebGL 不允许一个 pass 同时把同一张纹理作为输入和输出。粒子更新采用 ping-pong:本帧从 A 读取位置并写入 B,下一帧交换角色。

粒子纹理的通道可以保存经度、纬度、高度和年龄。高度不是地形高度,而是风场显示层中的归一化位置。面板的“风场厚度”把这个归一化值换算成米,使轨迹形成包裹地球的薄层。

vec2 wind = sampleWind(position.xy);
position.xy += wind * u_speedFactor * u_deltaTime;
position.w += u_deltaTime;

真实实现还要处理球面尺度:同样的经度差在高纬度对应更短距离,因此经向速度换算不能忽略 cos(latitude)。

轨迹不是保留所有历史点

案例把上一帧位置和当前帧位置组成短箭头,再用尾迹长度控制视觉延伸。它不在 CPU 中为每个粒子维护一个数组。否则 256K 粒子乘十几个历史点会造成大量内存和上传开销。

发光来自材质的颜色增强和混合方式,不是场景 Bloom。透明度过高时,大量粒子叠加会让地球变白;因此粒子数提高后通常还要降低单条轨迹 alpha。

面板参数的真实影响

参数改变什么是否重建
速度因子、衰减速度更新步长和存活概率否
箭头、尾迹、透明度轨迹几何或材质 uniform否
风速颜色范围色带归一化否
粒子数量状态纹理和实例数是
主题色带与一组显示参数否

setParticleCount 调用渲染器重建粒子池,然后再读取渲染器实际接受的数量。这样即使内部因为纹理尺寸限制进行了修正,Vue 面板也不会继续显示错误数值。

这个案例的主线是:Vue 管参数,GPU 管粒子状态,Cesium 管相机与地球场景。三层职责分开后,风场既能实时调节,也能在切换案例时完整回收。

从零复现:全球风场教程

案例源码位于 src/demos/cesiumdemos/wind-field/。实现时不要一开始就画粒子,先依次验证风场数据、GPU 状态和轨迹合成。

第 1 步:准备风场数据结构

至少准备经纬范围、横纵网格数以及展平后的 u/v 分量。数组索引约定必须固定为 index = row * width + column。

const wind = {
  west: -180,
  east: 180,
  south: -90,
  north: 90,
  width: 360,
  height: 181,
  u: new Float32Array(360 * 181),
  v: new Float32Array(360 * 181),
};

先在 CPU 随机抽十个网格点,打印经纬度和风速。方向颠倒时先修正行列或纬度起点,不要在 Shader 中用多个取反补救。

第 2 步:把 U/V 编码成纹理

创建一张浮点 RG 纹理,R 存 u,G 存 v。若设备不支持浮点纹理,则把数值归一化到 0–255,并额外传入最小值和最大值用于 Shader 解码。

for (let i = 0; i < wind.u.length; i += 1) {
  pixels[i * 2] = wind.u[i];
  pixels[i * 2 + 1] = wind.v[i];
}

完成标志:把风速纹理临时渲染到屏幕时,陆地附近的梯度连续,没有整行错位或南北翻转。

第 3 步:建立粒子状态 Ping-Pong

粒子状态纹理的 RG 保存经纬度,BA 可以保存寿命和随机种子。创建尺寸相同的 A/B 两张纹理和 Framebuffer:本帧从 A 读取并写入 B,下一帧交换引用。

let current = stateTextureA;
let next = stateTextureB;

function swapState() {
  [current, next] = [next, current];
}

禁止在同一个 pass 中读写同一张纹理。若粒子全部停在初始位置,优先检查 Framebuffer 附件和交换顺序。

第 4 步:在计算 Shader 中推进粒子

用粒子经纬度换算风场纹理 UV,双线性采样速度。中点法先预测半步位置,再在半步位置采样一次风速,比单次欧拉法在弯曲流场中稳定。

vec2 velocity0 = sampleWind(position);
vec2 midpoint = position + velocity0 * u_deltaTime * 0.5;
vec2 velocity1 = sampleWind(midpoint);
vec2 nextPosition = position + velocity1 * u_deltaTime;

随后处理跨日期线、越界和寿命结束。重生点使用粒子 seed 生成,不能每帧都由相同随机值重置。

第 5 步:生成有长度的轨迹

先把上一帧轨迹纹理乘衰减系数,再将粒子从旧位置到新位置的线段叠加进去。最后把轨迹纹理作为全屏 Primitive 合成到 Cesium 场景。

调试顺序是:关闭衰减查看是否持续累积 → 恢复衰减检查拖尾长度 → 修改线宽确认只影响显示、不影响粒子状态。

第 6 步:接入 Cesium 渲染循环

在 Primitive 的 update(frameState) 中按“粒子计算、轨迹衰减、线段绘制、屏幕合成”的顺序推送 DrawCommand。使用 frameState.context 创建 Cesium 管理的纹理和 ShaderProgram,销毁时逐一释放。

完成标志:旋转地球时轨迹仍贴在正确经纬度,暂停时间后粒子停止,修改粒子数只重建状态纹理而不会创建第二个动画循环。

第 7 步:建立参数成本表

粒子数影响状态纹理和每帧线段数量;速度影响积分步长;衰减影响拖尾;线宽只影响显示。把这四类参数分开处理,面板拖动时才不会无谓重建所有 GPU 资源。

验收与排错

  • 全部粒子向同一角落移动:检查 U/V 通道和经纬方向。
  • 日期线出现横贯全球的长线:绘制前检测经度跳变并拆段。
  • 相机移动时粒子漂离地球:检查经纬度到椭球位置的转换是否使用当前高度。
  • 粒子数量增加但画面不变:检查实例数和状态纹理尺寸是否同步更新。

相关文章

Cesium 北斗三维网格:在城市 3D Tiles 上做 GeoSOT-3D 剖分

介绍区域体素生成、建筑表面采样、结构碰撞检测、Worker 几何构建和网格编码查询

Cesium 区域等高线:地形采样与 Marching Squares

从交互绘制区域开始,完成规则网格采样、等高线提取、平滑、标注和高程统计