Vue3+Cesium 三维地球模拟雨雪雾天气粒子特效

发布时间:2026/11/3 10:39:59
Vue3+Cesium 三维地球模拟雨雪雾天气粒子特效
<template> <div class="cesium-box" ref="containerRef"></div> <div class="control"> <button @click="toggleRain">🌧️ 下雨</button> <button @click="toggleSnow">❄️ 下雪</button> <button @click="toggleFog">🌫️ 大雾</button> <button @click="clearAll">清空全部</button> </div> </template> <script setup> import { ref, onMounted, onUnmounted } from 'vue' import * as Cesium from 'cesium' import 'cesium/Build/Cesium/Widgets/widgets.css' const containerRef = ref(null) let viewer = null // 雨雪集合实例 let rainBillboardCollection = null let snowBillboardCollection = null let rainAnimateId = null let snowAnimateId = null let fogPostStage = null const state = { rain: false, snow: false, fog: false } // 相机中心点(西安,可自行修改经纬度) const centerLon = 108.94 const centerLat = 34.27 const range = 0.4 // 粒子分布范围 /** * 下雨 */ function toggleRain() { if (state.rain) { // 关闭下雨 cancelAnimationFrame(rainAnimateId) viewer.scene.primitives.remove(rainBillboardCollection) rainBillboardCollection = null state.rain = false return } state.rain = true rainBillboardCollection = new Cesium.BillboardCollection() viewer.scene.primitives.add(rainBillboardCollection) const count = 12000 // 粒子数据缓存,用来动画更新位置 const rainList = [] for (let i = 0; i < count; i++) { const lon = centerLon + (Math.random() - 0.5) * range const lat = centerLat + (Math.random() - 0.5) * range const height = 1500 + Math.random() * 2500 const item = { lon, lat, height, speed: 15 + Math.random() * 10, // 下落速度 wind: (Math.random() - 0.5) * 0.0008 // 风力偏移 } rainList.push(item) rainBillboardCollection.add({ position: Cesium.Cartesian3.fromDegrees(item.lon, item.lat, item.height), image: '/imgs/rain.png', scale: 0.2 + Math.random() * 0.15, color: Cesium.Color.fromAlpha(Cesium.Color.LIGHTBLUE, 0.4), disableDepthTestDistance: Number.POSITIVE_INFINITY }) } // 雨滴下落动画 function animateRain() { if (!state.rain) return for (let i = 0; i < count; i++) { const bill = rainBillboardCollection.get(i) const r = rainList[i] // 下落 + 风吹横向偏移 r.height -= r.speed r.lon += r.wind // 落到地面重置到上空循环 if (r.height < 0) { r.height = 3000 r.lon = centerLon + (Math.random() - 0.5) * range r.lat = centerLat + (Math.random() - 0.5) * range } bill.position = Cesium.Cartesian3.fromDegrees(r.lon, r.lat, r.height) } rainAnimateId = requestAnimationFrame(animateRain) } animateRain() } /** * 下雪 */ function toggleSnow() { if (state.snow) { cancelAnimationFrame(snowAnimateId) viewer.scene.primitives.remove(snowBillboardCollection) snowBillboardCollection = null state.snow = false return } state.snow = true snowBillboardCollection = new Cesium.BillboardCollection() viewer.scene.primitives.add(snowBillboardCollection) const count = 8000 const snowList = [] for (let i = 0; i < count; i++) { const lon = centerLon + (Math.random() - 0.5) * range const lat = centerLat + (Math.random() - 0.5) * range const height = 2000 + Math.random() * 3000 const item = { lon, lat, height, speed: 2 + Math.random() * 3, // 雪花下落更慢 wind: (Math.random() - 0.5) * 0.0012, swing: Math.random() * Math.PI * 2 // 左右摇摆相位 } snowList.push(item) snowBillboardCollection.add({ position: Cesium.Cartesian3.fromDegrees(item.lon, item.lat, item.height), image: '/imgs/snow.png', scale: 0.3 + Math.random() * 0.35, color: Cesium.Color.fromAlpha(Cesium.Color.WHITE, 0.75), disableDepthTestDistance: Number.POSITIVE_INFINITY }) } function animateSnow() { if (!state.snow) return const time = Date.now() * 0.001 for (let i = 0; i < count; i++) { const bill = snowBillboardCollection.get(i) const s = snowList[i] s.height -= s.speed s.lon += s.wind // 正弦模拟左右摇摆 s.lat += Math.sin(time + s.swing) * 0.0003 if (s.height < 0) { s.height = 4000 s.lon = centerLon + (Math.random() - 0.5) * range s.lat = centerLat + (Math.random() - 0.5) * range } bill.position = Cesium.Cartesian3.fromDegrees(s.lon, s.lat, s.height) } snowAnimateId = requestAnimationFrame(animateSnow) } animateSnow() } /** * 大雾:后期全屏雾,氛围感更强 */ function toggleFog() { if (state.fog) { viewer.scene.postProcessStages.remove(fogPostStage) fogPostStage = null state.fog = false return } state.fog = true fogPostStage = new Cesium.PostProcessStage({ fragmentShader: ` uniform sampler2D colorTexture; varying vec2 v_textureCoordinates; void main(void) { vec4 originColor = texture2D(colorTexture, v_textureCoordinates); vec3 fogColor = vec3(0.82, 0.85, 0.88); float fogPower = 0.38; vec4 finalColor = vec4(mix(originColor.rgb, fogColor, fogPower), originColor.a); gl_FragColor = finalColor; } ` }) viewer.scene.postProcessStages.add(fogPostStage) } // 清除全部天气效果 function clearAll() { if (state.rain) toggleRain() if (state.snow) toggleSnow() if (state.fog) toggleFog() } onMounted(() => { viewer = new Cesium.Viewer(containerRef.value, { terrainProvider: Cesium.createWorldTerrain(), animation: false, timeline: false, baseLayerPicker: false, geocoder: false, homeButton: false, sceneModePicker: false, navigationHelpButton: false }) // 定位到目标区域 viewer.camera.flyTo({ destination: Cesium.Cartesian3.fromDegrees(centerLon, centerLat, 4500), orientation: { heading: Cesium.Math.toRadians(0), pitch: Cesium.Math.toRadians(-25), roll: 0 }, duration: 2 }) // 开启深度,雾效果更好 viewer.scene.globe.depthTestAgainstTerrain = true }) onUnmounted(() => { clearAll() viewer?.destroy() }) </script> <style scoped> .cesium-box { width: 100%; height: 85vh; } .control { position: absolute; top: 14px; left: 14px; z-index: 99; display: flex; gap: 10px; } .control button { padding: 7px 14px; border: none; border-radius: 6px; background: #2478ff; color: #fff; cursor: pointer; } .control button:hover { background: #1b60d1; } </style>