Cesium上建立风场动态粒子效果
初学
使用Canvas + NCEP数据创建动态风场效果
封装基于Cesium的canvas渲染风场API
- 集成npm插件
-
https://www.npmjs.com/package/cesium-windy-canvas
-
npm i cesium-windy-canvas -S
- github传送门
- gitee传送门
windy.gif
甲、需要准备一个canvas
与Cesium
初始化后的图层
Cesium
初始化后的图层详见:vue3.0+vite+Cesium使用记录
讲解过程
1、先初始化Cesium
2、提供NCEP
风场数据
详见 cesium-windy-canvas/mock20万行风场数据,直接复制即可
3、在进行CanvasWindy
初始化
CanvasWindy
详见
cesium-windy-canvas
4、设置风场必要参数
5、非常简单的创建具有动态粒子效果的风场
CanvasWindy(data, params);
乙、设置Cesiumwindy
参数params
参数名 | 参数解释 | 类型 | 默认值 |
---|---|---|---|
viewer | Cesium初始化后赋值变量 | Object | null |
canvas | 风场画布 | DOM | null |
canvasContext | canvas上下文 | DOM | params.canvas.getContext('2d') |
canvasWidth | 画布宽度 | Number | 300 |
canvasHeight | 画布高度 | Number | 180 |
speedRate | 风前进速度 | Number | 100 |
extent | 风场绘制地图范围 | Array | [] |
particlesNumber | 粒子总数 | Number | 20000 |
maxAge | 每个粒子的最大生存周期 | Number | 120 |
frameTime | 每秒刷新次数 | Number | 100 |
lineWidth | 粒子线条宽度 | Number | 1 |
initExtent | 风场初始范围 | Array | [] |
calc_speedRate | 根据speedRate参数计算经纬度步进长度 | Array | [0, 0] |
windField | 风场网格 | Object | null |
particles | 风场粒子存储 | Array | [] |
animateFrame | requestAnimationFrame事件句柄,用来清除操作 | Object | null |
isdistory | 是否进行销毁 | bool | false |
windyColor | 风场颜色集合(根据风速不同设置不同颜色) | class | new WindyColor() |
丙、初始化风场粒子
在初始化
Cesiumwindy
构造函数时,需要传入风场数据,以及相应的参数
执行构造函数时自动执行init()进行粒子创造
丁、讲解根据风速设置不同粒子颜色
在初始化粒子中有一个画线的操作,在画线过程中进行颜色赋值
在使用this.particles
对所有粒子遍历进行画轨迹操作
创建粒子
/// randomParticle生成随机粒子
/// CanvasParticle() 粒子参数
this.particles.push(this.randomParticle(new CanvasParticle()));
CanvasParticle
参数解析
参数名 | 参数解释 | 类型 | 默认值 |
---|---|---|---|
lng | 粒子初始经度 | 度 | null |
lat | 粒子初始纬度 | 度 | null |
tlng | 粒子下一步将要移动的经度,这个需要计算得来 | 度 | null |
tlat | 粒子下一步将要移动的y纬度,这个需要计算得来 | 度 | null |
x | 粒子初始x位置 | Number | null |
y | 粒子初始y位置 | Number | null |
age | 粒子生命周期计时器,每次-1 | Number | null |
speed | 粒子移动速度 | Number | null |
color | 粒子颜色 | String | null |
给粒子赋值颜色
1、在生成随机粒子时根据风速计算不同颜色
this.windyColor.getWindColor(particle.speed)
详细见Cesiumwindy
源码第333行
2、画粒子轨迹线时进行画布粒子颜色添加
this.canvasContext.strokeStyle = particle.color
this.canvasContext.stroke();
详见Cesiumwindy
源码第244行
戊、至此风场粒子效果已经完成后续操作注意如下
1、粒子效果完成之后不会随着地图缩放变换,需要在操作之后重新进行渲染
(1)、电脑屏幕缩放
需要进行onresize
监听
window.onresize = resizeCanvas;
const resizeCanvas = () => {
/// windycanvas = document.getElementById('windycanvas') 画布
if (windycanvas == null) return;
windycanvas.width = window.innerWidth;
windycanvas.height = cesiumRef.value?.clientHeight;
if (windy) {
// 重新设置画布大小
// eslint-disable-next-line no-underscore-dangle
windy._resize(windycanvas.width, windycanvas.height);
}
};
(2)、地图缩放
地图缩放时需要对鼠标事件,滚轮事件进行监听
参考vue3.0+vite+Cesium使用记录中监听viewer操作事件
讲解
伪代码
handler = new Cesium.ScreenSpaceEventHandler(viewer.scene.canvas);
// 鼠标滚动、旋转后是否需要重新生成风场---如果需要,打开以下注释--旋转或者移动到北半球的时候计算会有问题
handler.setInputAction(() => {
clearTimeout(refreshTimer);
...设置风场隐藏
refreshTimer = setTimeout(() => {
if (windy) {
windy.extent = ...获取当前三维窗口左上、右上、左下、右下坐标集合;
windy.redraw();
}
...设置风场显示
}, 200);
}, Cesium.ScreenSpaceEventType.WHEEL);
// 鼠标左键、右键按下
handler.setInputAction(() => {
mouseDown = true;
}, Cesium.ScreenSpaceEventType.LEFT_DOWN);
handler.setInputAction(() => {
mouseDown = true;
}, Cesium.ScreenSpaceEventType.RIGHT_DOWN);
// 鼠标移动
handler.setInputAction(() => {
if (mouseDown) {
...设置风场隐藏
mouseMove = true;
}
}, Cesium.ScreenSpaceEventType.MOUSE_MOVE);
// 鼠标左键、右键抬起
handler.setInputAction(() => {
if (mouseDown && mouseMove) {
if (windy) {
windy.extent = globalExtent;
windy.redraw();
}
}
...设置风场显示
mouseDown = false;
mouseMove = false;
}, Cesium.ScreenSpaceEventType.LEFT_UP);
handler.setInputAction(() => {
if (mouseDown && mouseMove) {
if (windy) {
windy.extent = globalExtent;
windy.redraw();
}
}
...设置风场显示
mouseDown = false;
mouseMove = false;
}, Cesium.ScreenSpaceEventType.RIGHT_UP);
解释:
...设置风场显示
:在鼠标操作以及滚轮操作后将渲染后的风场重新进行展示,属于display
操作
...设置风场隐藏
:在鼠标操作以及滚轮操作时将画布进行隐藏
(3)、监听当前地球操作,时刻计算窗口坐标进行globalExtent
赋值
viewer.scene.postRender.addEventListener(() => {
getCesiumExtent();
});
// 获取当前三维窗口左上、右上、左下、右下坐标
const getCesiumExtent = () => {
const canvaswidth = window.innerWidth;
const canvasheight = cesiumRef.value?.clientHeight || 0 - 50;
// eslint-disable-next-line camelcase
const left_top_pt = new Cesium.Cartesian2(0, 0);
// eslint-disable-next-line camelcase
const left_bottom_pt = new Cesium.Cartesian2(0, canvasheight);
// eslint-disable-next-line camelcase
const right_top_pt = new Cesium.Cartesian2(canvaswidth, 0);
// eslint-disable-next-line camelcase
const right_bottom_pt = new Cesium.Cartesian2(canvaswidth, canvasheight);
const pick1 = viewer.scene.globe.pick(viewer.camera.getPickRay(left_top_pt), viewer.scene);
const pick2 = viewer.scene.globe.pick(viewer.camera.getPickRay(left_bottom_pt), viewer.scene);
const pick3 = viewer.scene.globe.pick(viewer.camera.getPickRay(right_top_pt), viewer.scene);
const pick4 = viewer.scene.globe.pick(viewer.camera.getPickRay(right_bottom_pt), viewer.scene);
if (pick1 && pick2 && pick3 && pick4) {
// 将三维坐标转成地理坐标---只需计算左下右上的坐标即可
const geoPt1 = viewer.scene.globe.ellipsoid.cartesianToCartographic(pick2);
const geoPt2 = viewer.scene.globe.ellipsoid.cartesianToCartographic(pick3);
// 地理坐标转换为经纬度坐标
const point1 = [(geoPt1.longitude / Math.PI) * 180, (geoPt1.latitude / Math.PI) * 180];
const point2 = [(geoPt2.longitude / Math.PI) * 180, (geoPt2.latitude / Math.PI) * 180];
// console.log(point1,point2);
// 此时说明extent需要分为东西半球
if (point1[0] > point2[0]) {
globalExtent = [point1[0], 180, point1[1], point2[1], -180, point2[0], point1[1], point2[1]];
} else {
globalExtent = [point1[0], point2[0], point1[1], point2[1]];
}
} else {
globalExtent = [];
}
};