【Vue3】Cesium实现雨雪效果
一、代码思路
1、📁项目结构
├─ src
│ ├─ assets
│ │ ├─ snowflake_particle.png // 雪花贴图
│ │ └─ circular_particle.png // 雨滴贴图
│ └─ components
│ └─ ParticleWeather.vue // 本文主角
└─ index.html
整个组件只有 一个文件,把它粘进任何 Vite / Vue3 工程即可运行。
2、🎨模板区:一张画布 + 一块控制面板
<div id="cesiumContainer" style="width: 100%; height: 100%;"></div>
-
这就是 Cesium 官方要求的“挂载点”,ID 必须叫
cesiumContainer
,后续new Cesium.Viewer('cesiumContainer')
会把整个 WebGL 地球塞进去。
<select v-model="weatherType" @change="changeWeather"><option value="snow">雪</option><option value="rain">雨</option><option value="none">无</option>
</select>
-
Vue3 的
v-model
把下拉框与weatherType
响应式绑定; -
@change="changeWeather"
只要切选项,就会自动销毁旧粒子系统并重建新的。
3、🚀Cesium 初始化:最简但够用
viewer = new Cesium.Viewer('cesiumContainer', {terrain: undefined, // 不加载地形,省显存baseLayerPicker: false, // 右上角图层选择器不要homeButton: false, // 主页按钮不要animation: false, // 时间轴控件不要...
});
scene = viewer.scene;
把配置项全部关掉,只保留“地球本体 + 星空 + 大气”。
随后立刻把相机拉到 正俯视 视角:
scene.camera.setView({destination: Cesium.Cartesian3.fromDegrees(0, 0, 10_000_000),orientation: { heading: 0, pitch: -90, roll: 0 }
});
-
fromDegrees(lon, lat, height)
把经纬度/高程变成三维世界坐标; -
heading = 0
代表机头朝北;pitch = -90
代表相机镜头朝下 90°; -
高度 10 000 km 正好能看到完整地球。
4、✨天气粒子系统:一次讲透 7 个核心参数
4.1 ❄️雪
scene.primitives.add(new Cesium.ParticleSystem({modelMatrix: Matrix4.fromTranslation(scene.camera.position),emitter: new Cesium.SphereEmitter(snowRadius),emissionRate: 7000,...updateCallback: snowUpdate})
)
-
modelMatrix
把粒子系统原点锁在 相机当前位置,因此无论用户怎么飞,雪都始终在下,实现“跟随视角”。 -
SphereEmitter(radius)
在半径 100 km 的球里随机吐雪花,看起来就像“漫天飞雪”。 -
emissionRate = 7000
每秒钟吐 7000 片雪花。改到 20 000 就会变成鹅毛大雪。 -
minimumImageSize / maximumImageSize
雪花最小 12×12 px,最大 24×24 px,随机变化,避免单调。
4.2 ❄️雪粒子更新回调 snowUpdate
snowGravityScratch = Cesium.Cartesian3.normalize(particle.position, snowGravityScratch);
Cesium.Cartesian3.multiplyByScalar(snowGravityScratch,Cesium.Math.randomBetween(-30, -300),snowGravityScratch
);
particle.velocity = Cesium.Cartesian3.add(particle.velocity,snowGravityScratch,particle.velocity
);
-
先把当前粒子位置 归一化 得到“向下”的单位向量(地球是球,向下方向各点不同);
-
乘一个 −30 ~ −300 的随机数,让雪花有快有慢;
-
叠加到
velocity
,实现重力加速度。
透明度根据 距相机距离 衰减:
const distance = Cartesian3.distance(scene.camera.position, particle.position);
particle.endColor.alpha = 1 / (distance / snowRadius + 0.1);
越靠近相机越不透明,越远离越透明,营造“近大远小”的体积感。
4.3 🌧️雨
雨滴贴图是长条形,所以:
JavaScript
复制
imageSize: new Cartesian2(15, 30)
雨滴下降速度固定 −1050 m/s(比雪快得多),其余思路与雪完全一致。
4.4 🌧️场景氛围
scene.skyAtmosphere.hueShift = -0.8;
scene.skyAtmosphere.saturationShift = -0.7;
-
整体色调偏冷(雪天);
-
雾效
fog.density
增加,远景被白雾吞噬,氛围感 +1。
5、📸相机控制:一键回到中国
viewer.camera.flyTo({destination: Cartesian3.fromDegrees(104, 30, 5_000_000),orientation: {heading: 0,pitch: Cesium.Math.toRadians(-60),roll: 0},duration: 2
})
-
flyTo
会带 2 秒平滑动画; -
104°E, 30°N
大致是中国中心; -
高度 5000 km,向下 60° 能看到东亚全貌。
6、🧹生命周期 & 资源清理
onMounted(initViewer);
onUnmounted(() => viewer.destroy());
-
viewer.destroy()
会把 WebGL 上下文、DOM、事件监听一次性销毁,避免内存泄漏; -
在 SPA 里来回切路由时尤其重要。
二、🌍工程实现
🛰️准备 Cesium 轨迹文件
官方地址:https://github.com/CesiumGS/cesium/tree/main/Apps/SampleData
下载circular_particle.png和snowflake_particle.png,将文件存放到工程的src\assets文件夹下
🛰️编写核心代码
创建src\components\ParticleWeather.vue,代码如下
<template><div style="width: 100%; height: 100vh; margin: 0; padding: 0; overflow: hidden;"><div id="cesiumContainer" style="width: 100%; height: 100%;"></div><div style="position: absolute; top: 20px; right: 20px; z-index: 100; background: rgba(0, 0, 0, 0.7); padding: 10px; color: white;"><h3>天气效果控制</h3><div style="margin-bottom: 10px;"><label>天气类型:</label><select v-model="weatherType" @change="changeWeather" style="margin-left: 5px;"><option value="snow">雪</option><option value="rain">雨</option><option value="none">无</option></select></div><button @click="resetCamera" style="margin-top: 10px; padding: 5px 10px; background: #4CAF50; color: white; border: none; cursor: pointer;">重置相机</button></div></div>
</template><script setup>
import { onMounted, onUnmounted, ref } from "vue";
import * as Cesium from "cesium";let viewer = null;
let scene = null;
const weatherType = ref('snow');// Cesium Ion 访问令牌(您需要替换为自己的令牌)
Cesium.Ion.defaultAccessToken = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJqdGkiOiIxM2M4ZTg1Ni0zYWFkLTRhMzMtYTE4My05MmZjNmY2YjAxNWYiLCJpZCI6MzI0MzUyLCJpYXQiOjE3NTMyODExOTJ9.FTPTi9u7zGDoZNOEeUq7kQxGEN2sn9NQuxGEY5bZAcI';// 雪粒子相关配置
const snowParticleSize = 12.0;
const snowRadius = 100000.0;
const minimumSnowImageSize = new Cesium.Cartesian2(snowParticleSize,snowParticleSize,
);
const maximumSnowImageSize = new Cesium.Cartesian2(snowParticleSize * 2.0,snowParticleSize * 2.0,
);
let snowGravityScratch = new Cesium.Cartesian3();// 雪粒子更新回调函数
const snowUpdate = function (particle, dt) {// 确保粒子位置有效if (!particle.position || isNaN(particle.position.x) || isNaN(particle.position.y) || isNaN(particle.position.z)) {return;}try {snowGravityScratch = Cesium.Cartesian3.normalize(particle.position,snowGravityScratch,);Cesium.Cartesian3.multiplyByScalar(snowGravityScratch,Cesium.Math.randomBetween(-30.0, -300.0),snowGravityScratch,);particle.velocity = Cesium.Cartesian3.add(particle.velocity,snowGravityScratch,particle.velocity,);// 根据距离相机的距离调整粒子透明度const distance = Cesium.Cartesian3.distance(scene.camera.position,particle.position,);if (distance > snowRadius) {particle.endColor.alpha = 0.0;} else {particle.endColor.alpha = 1.0 / (distance / snowRadius + 0.1);}} catch (error) {// 忽略归一化错误console.warn('Snow particle update error:', error);}
};// 雨粒子相关配置
const rainParticleSize = 15.0;
const rainRadius = 100000.0;
const rainImageSize = new Cesium.Cartesian2(rainParticleSize,rainParticleSize * 2.0,
);
let rainGravityScratch = new Cesium.Cartesian3();// 雨粒子更新回调函数
const rainUpdate = function (particle, dt) {// 确保粒子位置有效if (!particle.position || isNaN(particle.position.x) || isNaN(particle.position.y) || isNaN(particle.position.z)) {return;}try {rainGravityScratch = Cesium.Cartesian3.normalize(particle.position,rainGravityScratch,);rainGravityScratch = Cesium.Cartesian3.multiplyByScalar(rainGravityScratch,-1050.0,rainGravityScratch,);particle.position = Cesium.Cartesian3.add(particle.position,rainGravityScratch,particle.position,);// 根据距离相机的距离调整粒子透明度const distance = Cesium.Cartesian3.distance(scene.camera.position,particle.position,);if (distance > rainRadius) {particle.endColor.alpha = 0.0;} else {particle.endColor.alpha = Cesium.Color.BLUE.alpha / (distance / rainRadius + 0.1);}} catch (error) {// 忽略归一化错误console.warn('Rain particle update error:', error);}
};// 重置相机位置到中国上空
const resetCameraFunction = function () {if (scene) {// 使用flyTo动画平滑过渡到目标位置,确保用户能看到地球viewer.camera.flyTo({destination: Cesium.Cartesian3.fromDegrees(104.0, 30.0, 5000000), // 中国中心位置,高度5000公里orientation: {heading: Cesium.Math.toRadians(0.0), // 0度朝向北方pitch: Cesium.Math.toRadians(-60.0), // 向下60度roll: 0.0},duration: 2.0, // 飞行持续时间2秒complete: function() {console.log('Camera position reset to:', scene.camera.position.toString());}});}
};// 开始下雪效果
const startSnow = function () {if (!scene) return;// 移除所有现有粒子系统scene.primitives.removeAll();try {// 添加雪粒子系统scene.primitives.add(new Cesium.ParticleSystem({modelMatrix: new Cesium.Matrix4.fromTranslation(scene.camera.position),minimumSpeed: -1.0,maximumSpeed: 0.0,lifetime: 15.0,emitter: new Cesium.SphereEmitter(snowRadius),startScale: 0.5,endScale: 1.0,image: '/src/assets/snowflake_particle.png',emissionRate: 7000.0,startColor: Cesium.Color.WHITE.withAlpha(0.0),endColor: Cesium.Color.WHITE.withAlpha(1.0),minimumImageSize: minimumSnowImageSize,maximumImageSize: maximumSnowImageSize,updateCallback: snowUpdate,}),);// 调整天空大气层效果以增强雪景scene.skyAtmosphere.hueShift = -0.8;scene.skyAtmosphere.saturationShift = -0.7;scene.skyAtmosphere.brightnessShift = -0.33;scene.fog.density = 0.001;scene.fog.minimumBrightness = 0.8;} catch (error) {console.error('Failed to start snow effect:', error);}
};// 开始下雨效果
const startRain = function () {if (!scene) return;// 移除所有现有粒子系统scene.primitives.removeAll();try {// 添加雨粒子系统scene.primitives.add(new Cesium.ParticleSystem({modelMatrix: new Cesium.Matrix4.fromTranslation(scene.camera.position),speed: -1.0,lifetime: 15.0,emitter: new Cesium.SphereEmitter(rainRadius),startScale: 1.0,endScale: 0.0,image: '/src/assets/circular_particle.png',emissionRate: 9000.0,startColor: new Cesium.Color(0.27, 0.5, 0.7, 0.0),endColor: new Cesium.Color(0.27, 0.5, 0.7, 0.98),imageSize: rainImageSize,updateCallback: rainUpdate,}),);// 调整天空大气层效果以增强雨景scene.skyAtmosphere.hueShift = -0.97;scene.skyAtmosphere.saturationShift = 0.25;scene.skyAtmosphere.brightnessShift = -0.4;scene.fog.density = 0.00025;scene.fog.minimumBrightness = 0.01;} catch (error) {console.error('Failed to start rain effect:', error);}
};// 清除所有天气效果
const clearWeather = function () {if (!scene) return;// 移除所有粒子系统scene.primitives.removeAll();// 重置天空大气层和雾效scene.skyAtmosphere.hueShift = 0.0;scene.skyAtmosphere.saturationShift = 0.0;scene.skyAtmosphere.brightnessShift = 0.0;scene.fog.density = 0.00001;scene.fog.minimumBrightness = 0.0;
};// 切换天气效果
const changeWeather = function () {switch (weatherType.value) {case 'snow':startSnow();break;case 'rain':startRain();break;case 'none':clearWeather();break;}
};// 重置相机
const resetCamera = function () {resetCameraFunction();// 重置相机位置后重新创建粒子系统,使其随相机移动changeWeather();
};// 初始化 Cesium 查看器
const initViewer = () => {try {// 最简化的配置,仅保留必要功能viewer = new Cesium.Viewer('cesiumContainer', {// 不加载地形,减少复杂性terrain: undefined,shouldAnimate: true,// 不指定特定的影像提供商,使用默认值baseLayerPicker: false,homeButton: false,sceneModePicker: false,navigationHelpButton: false,infoBox: false,fullscreenButton: false,animation: false,timeline: false});scene = viewer.scene;// 确保地球可见的最基本配置scene.globe.show = true; // 显式设置地球可见// 禁用深度测试,简化渲染流程scene.globe.depthTestAgainstTerrain = false;console.log('Cesium viewer initialized with minimal configuration');// 立即设置相机位置,不使用动画scene.camera.setView({destination: Cesium.Cartesian3.fromDegrees(0.0, 0.0, 10000000), // 地球中心位置,高度10000公里orientation: {heading: 0.0,pitch: -90.0,roll: 0.0}});console.log('Camera position set to:', scene.camera.position.toString());// 延迟启动粒子系统,确保地球先显示出来setTimeout(() => {startSnow();}, 1000);// 初始化雪效果startSnow();// 监听相机移动事件,使粒子系统随相机移动viewer.scene.camera.changed.addEventListener(() => {if (weatherType.value !== 'none') {changeWeather();}});} catch (error) {console.error('Failed to initialize Cesium viewer:', error);}
};// 生命周期钩子
onMounted(() => {initViewer();
});onUnmounted(() => {if (viewer) {viewer.destroy();viewer = null;scene = null;}
});
</script>
参考:Cesium Sandcastle