【Vue3】Cesium实现雨雪效果

一、代码思路

1、📁项目结构

Lisp 复制代码
├─ 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
  })
)
  1. modelMatrix

    把粒子系统原点锁在 相机当前位置,因此无论用户怎么飞,雪都始终在下,实现"跟随视角"。

  2. SphereEmitter(radius)

    在半径 100 km 的球里随机吐雪花,看起来就像"漫天飞雪"。

  3. emissionRate = 7000

    每秒钟吐 7000 片雪花。改到 20 000 就会变成鹅毛大雪。

  4. 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,代码如下

Lisp 复制代码
<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

相关推荐
ZZHow10241 天前
Java项目-苍穹外卖_Day1
java·spring boot·web
带刺的坐椅1 天前
老码农教你 Solon Web Context-Path 的两种配置方式
java·nginx·tomcat·web·solon
ZZHow10241 天前
Java项目-苍穹外卖_Day2
java·spring boot·web
OEC小胖胖4 天前
【React 设计模式】受控与非受控:解构 React 组件设计的核心模式
前端·react.js·设计模式·前端框架·web
蒋星熠4 天前
全栈开发:从LAMP到云原生的技术革命
微服务·云原生·职场和发展·架构·系统架构·web·devops
诗人不说梦^6 天前
[NCTF2019]True XML cookbook
web·ctf
hui函数6 天前
Flask-WTF表单验证全攻略
后端·python·flask·web·表单验证
OEC小胖胖7 天前
【React Hooks】封装的艺术:如何编写高质量的 React 自-定义 Hooks
前端·react.js·前端框架·web
练习时长两年半的Java练习生(升级中)7 天前
从0开始学习Java+AI知识点总结-18.web基础知识(Java操作数据库)
java·学习·web