文章目录
这个案例把全球风场变成一组贴着地球运动的发光粒子。粒子的位置、速度和生命周期都在 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 粒子的更新链
一帧可以拆成四步:
- 从风场纹理读取当前经纬度对应的风速。
- 按速度因子推进粒子位置。
- 对越界、速度过低或寿命结束的粒子重新播种。
- 把上一帧位置和当前帧位置渲染成带方向的轨迹。
粒子状态适合放在浮点纹理中。一次计算会同时更新大量像素,每个像素就是一个粒子。选择 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 通道和经纬方向。
- 日期线出现横贯全球的长线:绘制前检测经度跳变并拆段。
- 相机移动时粒子漂离地球:检查经纬度到椭球位置的转换是否使用当前高度。
- 粒子数量增加但画面不变:检查实例数和状态纹理尺寸是否同步更新。