一、代码思路
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 轨迹文件
下载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>