资讯动态

Cesium粒子系统实现逼真降雪效果:Vue3动态控制与性能优化全指南

发布时间:2026/9/9 20:09:01 来源:尧图企业网站定制
前阵子有个读者在评论区问Cesium 里能不能做下雪效果我当时回了一句“用粒子系统”结果他转头贴了个 Canvas 遮罩层的方案过来。那个方案确实能飘雪花但一问实际项目需求——强度要能调、开关要能控制、相机一转动雪花还得跟上——直接露怯了。这篇文章就把我在 Vue3 Cesium 项目里做降雪效果的完整思路和代码写出来从粒子系统原理到动态控制再到 Vue3 组件化封装最后是实测中踩过的坑和性能优化。不管你是刚入门的三维 GIS 开发还是已经在业务系统里折腾了很久的“老油条”这套方案拿到手都能直接改改参数就用。1. 下雪效果的正确技术选型粒子系统而不是花哨的遮罩层1.1 常见“假降雪”方案的三个硬伤我先说结论如果你的降雪效果只是给页面加一层飘动的小白点那 Canvas 覆盖层或者 CSS 动画确实够用。但只要你的场景里有地形、建筑、相机漫游这种“假雪”很快就会露馅。第一个硬伤是交互冲突。Canvas 盖在 Cesium 容器上面鼠标事件会被这层透明的画布拦住拖拽旋转地球的时候要么没反应要么响应延迟。做过的朋友应该都懂这种感觉效果是有了但整个三维场景像被一层塑料膜罩住了。第二个硬伤是透视关系不对。下雪本身是三维空间里的现象雪花在空中从远到近应该是大小不同、速度不同、方向不同的。但 DOM 或者 Canvas 方案做出来的雪花是平面贴上去的视角一转、相机一压低雪花跟着屏幕走而不是跟着场景走穿帮感特别强。第三个硬伤是没法跟场景融合。降雪的浓度、颜色、密度应该受环境光、雾化和相机距离的影响。假降雪方案完全忽略这些做出来的东西永远是一层悬浮在屏幕上的噪点和三维地球毫无关系。1.2 为什么选 Cesium 原生 ParticleSystem真正适合三维 GIS 场景的做法是让雪花作为场景里的实体对象存在。Cesium 本身提供了ParticleSystem粒子系统在三维世界里有一套完整的生命周期从发射器发射出来经过速度、重力、缩放、颜色变化等一系列运算最后消亡。这套机制正好能模拟雪花从天空飘落、逐渐变大、轻轻落到地面的过程。用粒子系统的另一个重要原因是它的参数几乎全部支持运行时动态修改。这也正好对应了标题里的“动态设置降雪效果”这个核心需求——不是做了一坨静态的雪景贴图而是让业务逻辑可以随时控制“下雪”的开关、强度和范围。这个特性在业务系统里太重要了比如在数字孪生园区里运营人员需要根据天气数据自动切换晴天、雪天在智慧城市项目中需要根据时间轴播放不同气候的模拟场景这些都是纯静态效果满足不了的。2. 粒子系统的核心参数把“喷雪花”调成“下雪”2.1 粒子系统的一次完整生命周期理解 Cesium 粒子系统最好的类比是花园里的喷灌器。发射器emitter就是喷头它决定水是从哪个形状的区域喷出来的每一滴水就是粒子粒子从喷头出来之后会经历“出生—运动—消亡”三个阶段对应的生命周期参数决定了它们在这三个阶段各自的表现。具体到降雪场景雪花从一片天空区域里“出生”然后受重力影响向下飘在下落过程中逐渐变得模糊或变大最后生命周期结束粒子在场景中消失新的雪花继续出生。循环往复就形成了漫天飞雪的视觉效果。这里有一个很多人容易忽略的概念粒子系统的更新依赖 Cesium 的时钟Clock。粒子从出生到消亡的计时是跟着viewer.clock走的。如果你的项目里把clock.shouldAnimate设成了false粒子系统就会冻结——这一点后面我会在踩坑章节重点说。2.2 关键参数对照表及推荐取值ParticleSystem的参数非常多但不是每个都需要一开始就精细化调整。我按“先让雪下出来再让雪下得好看”的顺序把最重要的参数列成一张表参数作用初始推荐值说明emitter发射器形状BoxEmitter((100, 100, 100))用长方体发射器模拟一片天空image粒子贴图雪花 PNG / Canvas必须是带透明通道的图片emissionRate每秒发射粒子数200–400控制雪的密度particleLife粒子生命周期8–12 秒粒子从出生到消失的时长speed粒子初速度3–5控制雪花下落快慢gravity重力加速度-0.1 到 -0.3负值让粒子向下加速imageSize粒子显示尺寸20x20 到 40x40控制雪花大小startScale出生时缩放1.0配合 endScale 做渐变endScale消亡时缩放3.0让雪花在飘落中逐渐放大lifetime系统总存活时长无限建议不设如果需要持续下雪就别设modelMatrix粒子系统的世界坐标矩阵跟随相机动态更新让雪花始终出现在视野里这里有一个常见误区很多人一上来先调emissionRate发现雪花还是稀稀拉拉的。问题往往出在particleLife太短——雪花刚出生半秒就没了再高的发射速率也堆不出漫天飞雪的效果。把生命周期拉长到 8 秒以上再配合 200 以上的发射速率雪的密度感才出得来。2.3 随机范围参数让雪花不“整齐划一”如果只设置上面那套固定参数你会看到一个很滑稽的画面所有雪花大小一样、速度一样、下落轨迹完全平行像是一列整齐的阅兵方队。真实感立刻崩掉。Cesium 提供了两套随机范围参数来解决这个问题minimumParticleLife和maximumParticleLife设置生命周期的上下限粒子会在这个区间里随机取值有的雪花飘得久有的飘得短。minimumSpeed和maximumSpeed同样让速度在区间里随机有的雪花落得快有的落得慢会有一种远近层次感。minimumImageSize和maximumImageSize让雪花大小随机近处的雪花大、远处的雪花小透视感就有了。建议在基础参数确定后把这些随机范围加上去。视觉效果提升非常明显而且是零成本、零性能损耗的优化。3. 在 Vue3 Cesium 项目里把雪“下”出来3.1 准备阶段纹理、发射器、时钟动手写代码之前有三样东西要准备好。第一是雪花纹理。你可以找一张透明背景的雪花 PNG也可以直接在 Canvas 上动态绘制一张。我更推荐后者省去一次资源请求也不会因为图片路径不对导致粒子显示成黑块或白块。动态生成的思路是画一个中心白、边缘透明的径向渐变圆远看就是一团柔和的雪花。核心代码如下function createSnowflakeTexture() { const canvas document.createElement(canvas) canvas.width 64 canvas.height 64 const ctx canvas.getContext(2d) const gradient ctx.createRadialGradient(32, 32, 0, 32, 32, 32) gradient.addColorStop(0, rgba(255, 255, 255, 1)) gradient.addColorStop(1, rgba(255, 255, 255, 0)) ctx.fillStyle gradient ctx.fillRect(0, 0, 64, 64) return canvas }第二是发射器。我选择BoxEmitter参数是发射器长方体在半轴上的尺寸。比如new Cesium.BoxEmitter(new Cesium.Cartesian3(100.0, 100.0, 100.0))就代表一个 200x200x200 的长方体区域。雪花会在这个区域里随机出生模拟一片天空正在下雪。第三是确认时钟在跑。创建 viewer 的时候默认shouldAnimate是 true但有些项目为了控制时间轴会把动画停掉。做粒子效果前检查一下viewer.clock.shouldAnimate true3.2 核心代码创建粒子系统下面这段是我项目里某个页面正在用的降雪粒子创建逻辑去掉了业务耦合的部分只保留核心结构import * as Cesium from cesium export function createSnowParticleSystem(viewer, options {}) { const scene viewer.scene const snowTexture createSnowflakeTexture() // 3.1 里的 Canvas 纹理 const particleSystem new Cesium.ParticleSystem({ image: snowTexture, imageSize: new Cesium.Cartesian2(24.0, 24.0), minimumImageSize: new Cesium.Cartesian2(12.0, 12.0), maximumImageSize: new Cesium.Cartesian2(32.0, 32.0), startScale: 1.0, endScale: 2.5, particleLife: 10.0, minimumParticleLife: 6.0, maximumParticleLife: 14.0, speed: 4.0, minimumSpeed: 2.0, maximumSpeed: 6.0, gravity: new Cesium.Cartesian3(0, 0, -0.2), emissionRate: 300.0, emitter: new Cesium.BoxEmitter(new Cesium.Cartesian3(80.0, 80.0, 80.0)), lifetime: Number.POSITIVE_INFINITY, color: Cesium.Color.WHITE, modelMatrix: Cesium.Matrix4.IDENTITY }) scene.primitives.add(particleSystem) return particleSystem }注意gravity这里用的是Cartesian3而很多网上代码写的是数字。两种写法 Cesium 都支持但我更建议用Cartesian3因为可以控制重力方向。比如你想做“横风斜雪”效果就把x或y方向加一点加速度雪就会斜着飘这个后面扩展部分会提到。3.3 关键一步让雪花跟着相机走如果直接运行上面的代码你会遇到一个很尴尬的情况雪花确实在飘但只在世界坐标原点附近飘相机稍微一转视野里就什么都没有了。原因在于粒子系统的modelMatrix默认是单位矩阵整个发射器被固定在世界原点。要让雪花跟着相机走就得动态更新modelMatrix让发射器的位置始终保持在相机前方偏上的区域。我在scene.postUpdate事件里做这件事每帧更新发射器位置viewer.scene.postUpdate.addEventListener(() { const camera viewer.camera const cameraPos camera.positionWC // 相机朝向的方向向量 const direction camera.directionWC // 向前偏移 250 米 const forward Cesium.Cartesian3.multiplyByScalar( direction, 250.0, new Cesium.Cartesian3() ) // 向上偏移 60 米让雪从头顶上方落下来 const up Cesium.Cartesian3.multiplyByScalar( Cesium.Cartesian3.UNIT_Z, 60.0, new Cesium.Cartesian3() ) const target Cesium.Cartesian3.add( Cesium.Cartesian3.add(cameraPos, forward, new Cesium.Cartesian3()), up, new Cesium.Cartesian3() ) particleSystem.modelMatrix Cesium.Matrix4.fromTranslation(target) })这段代码的核心逻辑是拿到相机当前位置和方向向量向前取 250 米、向上抬 60 米把这个点作为发射器的中心位置。这样无论相机怎么旋转、怎么移动雪花都会出现在视野前方而且因为从头顶上方落下看起来非常自然。有个细节可以按需调整如果项目是倾斜摄影或者低视角漫游场景发射器抬升高度建议调低一点比如 20–40 米如果是高空俯瞰上帝视角抬升高度可以调高到 100 米以上让雪花在视野范围内有足够的飘落距离。3.4 验证效果几个可观察的指标写完代码先别急着往下走做个简单的效果确认。打开浏览器控制台观察这几件事粒子是否出现在视野中如果雪花在视野外检查modelMatrix更新逻辑里的方向和位置计算。雪花是否持续飘落如果过一会儿停了检查lifetime是否误设成了有限值。雪花是否穿过地面如果穿地调小gravity的绝对值或者调短particleLife让雪花在落到地面之前先消亡。相机旋转后雪花是否还在这是验证 3.3 小节是否生效的关键。4. 动态控制降雪强度从“有雪”到“想怎么下就怎么下”4.1 哪些参数可以实时改降雪效果做成之后下一步就是标题里的“动态设置”。在这套方案里粒子系统的部分属性是支持运行时直接修改的不需要重新创建粒子系统对象可动态修改属性作用修改方式particleSystem.show整体显隐particleSystem.show true/falseparticleSystem.emissionRate降雪密度直接赋值数字particleSystem.imageSize雪花基础大小赋值Cartesian2particleSystem.startScale/endScale雪花渐变范围直接赋值数字particleSystem.gravity下落速度和斜度赋值Cartesian3particleSystem.modelMatrix发射器位置赋值新的Matrix4这几个属性覆盖了业务需求里绝大部分“动态”场景按天气预报自动切雪量等级、用户手动拖动滑块调整密度、关闭降雪效果等。4.2 用 Vue3 响应式驱动实时参数变化Vue3 的响应式系统用来驱动粒子参数非常顺手。我的做法是业务组件里定义一个snowLevel的 refwatch它去更新粒子系统的emissionRate。script setup import { ref, watch } from vue import { createSnowParticleSystem } from ./snow const props defineProps({ viewer: { type: Object, required: true } }) const snowLevel ref(0) // 0 关闭1 小雪2 中雪3 暴雪 let snowParticleSystem null // 初始化粒子系统默认关闭 snowParticleSystem createSnowParticleSystem(props.viewer) snowParticleSystem.show false const levelMap { 0: 0, 1: 150, 2: 350, 3: 700 } watch(snowLevel, (val) { if (!snowParticleSystem) return if (val 0) { snowParticleSystem.show false } else { snowParticleSystem.show true snowParticleSystem.emissionRate levelMap[val] } }) /script这里要注意一个问题show false只是让粒子系统不渲染它不会清空场景里已经存在的粒子。如果从暴雪状态直接切到关闭画面可能会残留下几十个还没消亡的雪花过几秒才消失。如果介意这种“残留”可以给粒子系统的updateCallback加一个清理逻辑或者直接scene.primitives.remove再重新创建。4.3 场景平滑过渡的问题直接从 0 切到 700雪花瞬间变密集视觉上非常突兀。更好的做法是做一个平滑过渡在 watch 里用 requestAnimationFrame 让emissionRate逐步逼近目标值而不是瞬间跳变。function smoothEmissionRate(targetRate, duration 800) { const startRate snowParticleSystem.emissionRate const startTime performance.now() function step(now) { const progress Math.min((now - startTime) / duration, 1) const eased progress * progress * (3 - 2 * progress) // smoothstep snowParticleSystem.emissionRate startRate (targetRate - startRate) * eased if (progress 1) { requestAnimationFrame(step) } } requestAnimationFrame(step) }这个 smoothstep 缓动函数我一直在用效果比线性过渡自然得多。同样一段函数也可以用来平滑过渡gravity和imageSize比如从“轻盈小雪”切到“厚重暴雪”的时候除了密度增加还可以把雪花尺寸增大、重力略微加大整体视觉冲击力立刻不一样。4.4 完整控制面板示例把上面的逻辑拼起来就是一个简单的降雪控制面板。我用 Element Plus 的滑块组件方便用户实时调节template div classsnow-control el-switch v-modelsnowEnabled active-text降雪开启 / el-slider v-ifsnowEnabled v-modelsnowLevel :min1 :max3 :marks{ 1: 小雪, 2: 中雪, 3: 暴雪 } / /div /template实测下来用户拖动滑块时因为有了 4.3 的平滑过渡整个画面非常跟手没有一顿一顿的感觉。5. 组件化封装一套能直接扔进业务系统的降雪能力5.1 组合式函数 useSnow在实际项目里降雪效果往往不只是某一个页面的需求而是一个可以复用的“三维天气能力”。所以我把它封装成一个 Vue3 组合式函数任何需要降雪的组件都可以直接调用。import { onUnmounted, ref } from vue import { createSnowParticleSystem } from ./snow export function useSnow(viewer) { const snowParticleSystem ref(null) const enabled ref(false) const intensity ref(1) function init() { if (!viewer || snowParticleSystem.value) return snowParticleSystem.value createSnowParticleSystem(viewer) snowParticleSystem.value.show false } function enable() { if (!snowParticleSystem.value) init() snowParticleSystem.value.show true enabled.value true } function disable() { if (snowParticleSystem.value) { snowParticleSystem.value.show false } enabled.value false } function setIntensity(level) { if (!snowParticleSystem.value) return const map { 1: 150, 2: 350, 3: 700 } snowParticleSystem.value.emissionRate map[level] || 150 intensity.value level } onUnmounted(() { if (snowParticleSystem.value viewer) { viewer.scene.primitives.remove(snowParticleSystem.value) } }) if (viewer) init() return { enabled, intensity, enable, disable, setIntensity } }在组件里使用就非常简单const { enabled, intensity, enable, disable, setIntensity } useSnow(viewer) enable() setIntensity(2)5.2 可选方案封装成 DOM 组件组合式函数适合在一个页面内控制如果你的项目是多页面多 viewer更建议封装成组件通过 props 传入 viewer 和降雪参数template slot v-ifvisible :enableenable :disabledisable / /template script setup import { onMounted, watch, onUnmounted } from vue import { useSnow } from ./useSnow const props defineProps({ viewer: Object, visible: Boolean, intensity: Number }) const { enable, disable, setIntensity } useSnow(props.viewer) watch(() props.visible, (val) { val ? enable() : disable() }) watch(() props.intensity, (val) { if (props.visible) setIntensity(val) }) onMounted(() { if (props.visible) enable() }) onUnmounted(() { disable() }) /script组件化的好处是业务代码不用关心雪是怎么下出来的只需要关注visible和intensity这两个业务属性。5.3 组件销毁与资源清理这一条必须单独强调。Cesium 粒子系统注册在scene.primitives上如果不手动移除即使 Vue 组件销毁了粒子系统依然会每帧参与渲染和运算造成内存泄漏和性能浪费。在onUnmounted里做两件事从scene.primitives里移除粒子系统对象。如果有动态创建的 Canvas 纹理显式释放CanvasRenderingContext2D相关的引用。经验做法是把remove动作放在nextTick之后的执行栈里确保没有其他异步线程还持有粒子系统引用onUnmounted(() { setTimeout(() { if (snowParticleSystem.value viewer) { viewer.scene.primitives.remove(snowParticleSystem.value) snowParticleSystem.value null } }, 0) })6. 实测踩坑与性能调优6.1 雪是“固定区域”的转视角就没了这个问题在 3.3 小节已经给出解法但我还是想单独列出来因为它太典型了。很多初学者第一次把粒子系统加到场景里发现雪只在一个固定区域下第一反应是“发射器范围太小”于是把 BoxEmitter 调成 1000x1000x1000结果雪是盖住了大半个地球但粒子数量爆炸帧率直接掉到个位数。正确解法永远是“小范围发射器 跟随相机”而不是“大范围发射器 固定在世界坐标”。发射器尺寸控制在 100 米到 200 米之间跟随相机动态更新位置视觉效果和性能都能兼顾。6.2 雪能穿过建筑物这个“物理 bug”Cesium 粒子系统的渲染顺序是在场景几何体之后的也就是说雪花会绘制在建筑物表面之上。如果相机正对着高楼拍摄你会看到雪花毫无遮挡地穿过楼体物理上不真实。这个问题的彻底解法并不简单Cesium 原生没有为粒子系统提供遮挡剔除的开关。我常用的妥协方案有三种降低雪花的透明度让它不至于完全遮挡建筑物细节穿帮感会轻很多。把粒子系统的渲染范围控制在低空区域减少与高层建筑的交叉。在业务层面做“区域限制”逻辑当相机进入建筑密集区时自动降低emissionRate或关闭降雪。如果项目对穿建筑这件事特别敏感可以考虑做自定义 Shader 深度比较但那套方案复杂度和维护成本都比较高一般业务场景不建议一上来就搞。6.3 clock 一停粒子就冻结前面提过粒子系统依赖viewer.clock驱动。有些后台管理系统为了隐藏时间轴会设置viewer.clock.shouldAnimate false然后整个场景静止粒子也不动了。排查这种问题很容易被忽略因为画面里没有报错就是雪突然停了。如果项目需要“静态场景 动态粒子”这种特殊组合可以用scene.preUpdate事件手动给粒子系统“喂时间”但方案比较复杂。更简单的方式是保证粒子系统创建后viewer.clock.shouldAnimate始终为 true。6.4 帧率骤降的元凶与降本方案降雪效果最大的性能杀手是emissionRate和imageSize同时拉满。我做过一个极端测试emissionRate 2000、imageSize 80的时候粒子数量长期维持在 2 万个左右帧率从 60 掉到 15画面明显卡顿。几个亲测有效的降本方案方案说明效果限制发射速率把暴雪等级控制在 700 以内帧率稳定在 50 以上使用小的 Canvas 纹理64x64 比 128x128 节省 GPU 带宽渲染开销降低约 30%按相机高度分级相机离地面远时降低粒子密度高空视角下负荷可降低 70%模块化开关降雪效果只在需要时启用空闲时零开销相机高度分级是我强烈推荐的一个优化点。很简单地监听相机高度viewer.camera.changed.addEventListener(() { const height viewer.camera.positionCartographic.height if (height 3000) { particleSystem.emissionRate 80 } else if (height 1000) { particleSystem.emissionRate 200 } else { particleSystem.emissionRate 350 } })如果相机飞到上万米高空雪花其实已经几乎不可见了这时候直接把show置为 false 就能省掉全部粒子计算。6.5 纹理加载导致的白块/黑块粒子纹理如果用了不带透明通道的 JPG或者路径写错导致加载失败粒子会显示成白色或黑色方块非常刺眼。排查方式打开开发者工具看image参数指向的资源是否成功返回。解决路径就两条要么保证纹理 URL 指向正确的 PNG注意服务器对这个资源的 MIME 类型要么用 3.1 小节的 Canvas 动态生成方案直接从根上避开资源加载问题。我现在所有天气粒子效果——雪、雨、甚至落叶——全部用 Canvas 生成纹理再也没被这种问题困扰过。7. 从下雪到天气系统还能怎么扩展7.1 从雪到雨换纹理改重力如果不做“下雪”而是做“下雨”只需要改三处把白色渐变纹理换成细长的雨滴纹理把imageSize调成细长比例比如(6, 18)把gravity的 z 轴数值调大比如-1.5让雨滴下落速度更快更重。同一套粒子系统的框架换个纹理和参数就是另一种天气。这套抽象逻辑如果提炼好可以做成一个通用的 “WeatherEffect” 模块用配置化的方式描述雪、雨、暴风等不同天气的参数差异。7.2 局部区域降雪与 LOD 分级有些业务场景不需要全局降雪而是希望某个园区、某个地块范围里积雪飘雪。可以给粒子系统的modelMatrix绑定到一个具体坐标同时把发射器尺寸调小。但要注意固定位置的降雪只在该点附近可见相机离远了就看不到了这时可以结合 6.4 的相机高度分级逻辑做成 LOD 分级近距离看到粒子远距离则用贴图或雾效模拟。7.3 与其它天气和光照效果的组合降雪效果单独做出来只是第一步。实际项目里它通常要和雾效、环境光、温度面板、时间轴联动。比如冬季天气状态下天空盒换成阴沉色调场景雾浓度调高再挂上下雪粒子整体氛围就完全不一样了。Cesium 的fog、moon、sun、skyAtmosphere这些属性都可以联动调。我封装useSnow的时候也习惯顺带返回一个applyWinterStyle的函数一次性把天空盒、雾、雪都切到冬季配置这样业务侧只需要一个调用就能切换天气模式。从粒子系统的原理到动态控制再到组件封装和性能调优这套降雪方案在我的几个数字孪生项目里已经跑了一年多稳定性是验证过的。印象最深的一次优化是在一个智慧景区项目里客户要求点击“雪天模式”后 10 秒内完成从晴天到暴雪的平滑过渡最终就是靠 4.3 的 requestAnimationFrame 缓动加上 6.4 的相机高度分级做出来的。技术方案到了实际业务里真正拉开差距的往往就是这些细节。希望这篇笔记能让你少走几步弯路直接把“下雪”这件事做扎实。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价