ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

Vue3 + Cesium 实现三维场景闪电雷雨天气效果全解析

2026/9/19 13:22:25 拓冰建站 浏览量
Vue3 + Cesium 实现三维场景闪电雷雨天气效果全解析 像“暴雨中的城市”这种天气效果其实不只是视觉Demo里才用到——智慧园区的数字孪生、港口调度模拟、甚至影视预演里都会频繁出现。我之前给一个三维可视化大屏项目做天气仿真时接到了“在Vue3里用Cesium做一套闪电加雷雨”的需求。刚拿到这个需求时觉得不难但真正把粒子系统、动态光照、随机事件调度揉到一起以后才发现这里面的细节多到能写一篇长文。这篇文章就专门拆解这套“Vue3 Cesium闪电雷雨效果”怎么做。我会从框架选型和底层API的取舍开始讲然后带着你把乌云、雨滴、闪电三个核心效果逐一实现再补充性能优化和踩坑记录。整套方案不依赖第三方特效库纯用Cesium原生能力加Vue3逻辑层实现适合正在做数字孪生、可视化大屏或WebGIS天气仿真的前端兄弟参考。1. 项目整体设计与技术选型1.1 为什么是Vue3 Cesium这套组合先说明一下我为什么锁死这套技术栈。Cesium本身是面向WebGL的开源三维地球引擎渲染大范围地形、倾斜摄影、3DTiles模型是它的强项。而Vue3作为前端框架负责UI交互、数据状态、组件生命周期。两者定位完全不同组合在一起正好覆盖“界面层”和“渲染层”。选Vue3而不是Vue2除了响应式系统和组合式API带来的代码组织便利之外更重要的是Vue3的生态已经非常成熟。像vite创建工程、自动按需引入、TS支持这些对集成大体积的Cesium库都很友好。我实际开发中大量用到ref和onUnmounted来管理Cesium viewer的生命周期在Vue2时代这种写法要么靠this.$once黑魔法要么写一堆beforeDestroy事件解绑代码体验差很多。不过Cesium和Vue3之间有一点必须提前想清楚Cesium的内部对象viewer、primitive、material绝不能直接用Vue的reactive()或ref()包裹。Cesium每帧都在高频更新这些对象的属性如果丢进Vue响应式系统会导致大量无意义的依赖收集和触发更新帧率会被瞬间拖垮。正确做法是让Vue3只负责“命令下发”——比如用户点击按钮触发暴雨我们通过普通函数调用Cesium API去改变渲染状态而不是驱动一个响应式的雨量变量。注意shallowRef和markRaw这两个API在Vue3集成Cesium时非常实用。我用markRaw把初始化好的viewer实例标记为“永不响应式”能有效避免Vue3代理对Cesium内部的劫持。1.2 Entity与Primitive的对比和取舍我猜很多人卡在“用Entity还是Primitive”这个问题上。结合热词里的疑问我直接说结论如果你的场景是给客户交差、只做一次性展示Entity完全够用如果是面向数字孪生这种长期运行、需要同时渲染大量对象的项目必须混合使用Primitive。Entity的优点是API简洁一个viewer.entities.add()就能添加点线面、模型、广告牌而且自带拾取、弹窗这些上层能力。缺点也很明显每个Entity都是独立对象底层会被Cesium封装成独立的DrawCommand100个闪电折线就是100个绘制命令性能瓶颈非常明显。Primitive则是更底层的渲染对象开发者需要手动管理Geometry、Appearance、材质这些概念控制粒度更细可以把同类几何体合并到同一个DrawCommand里大批量渲染时才扛得住。代价是代码难度直接上一个台阶。我在雷雨效果里的取舍是这样的效果选型原因雨滴粒子ParticleSystemCesium内置属于底层系统API清晰闪电分支自定义PolylineCollection可集合多条线段为一次绘制减少DrawCommand乌云体自定义Primitive 着色器需要半透明混合和不规则扰动Entity无法精细控制地面积水反光Ellipsoid Primitive 自定义Material动态修改uniform实现雨滴涟漪Entity不是不能用但要克制。比如要在某个位置标记雷雨中心点放一个Entity的广告牌没问题但如果要一次性生成50条雨线、每帧更新坐标用Entity就是给自己挖坑。1.3 动态光照和仿真效果的方案解构“闪电雷雨”看似是一个整体效果实际拆开之后是四个互相独立但又需要协同的系统乌云压顶、暴雨粒子、闪电光效、环境光照变化。先说动态光照。Cesium官方其实提供了一套光照模型默认的太阳光是跟随真实时间变化的。我们要做的是“闪电瞬间照亮一切”——这种效果不能靠改太阳位置实现更合理的做法是用Cesium的PostProcessStage在闪电闪白的那0.1到0.2秒叠加一个全屏的发光材质再配合粒子颜色的同步提亮模拟出电光火石间的强光。雷雨粒子也不单是“蓝色半透明粒子往下掉”这么简单。真正看起来真实的雨滴是细长的轨迹、有深浅层次、且有风场干扰。Cesium的ParticleSystem内置了ParticleEmitter发射器和UpdateCallback粒子回调函数我们能直接在回调里给每个粒子追加重力加速度、随机扰动就能让粒子轨迹拉出雨丝的效果。闪电路径更是个经典算法题如何生成一束自然的闪电我采用的是递归分形中点位移算法。先把闪电起点和终点确定在中间点按随机幅度做偏移然后对每一段再递归细分每递归一次偏移量减半这样就能得到一道有主干的闪电周围还能再生成一些细小的分支。具体算法我在第3节详细展开。2. 环境准备与基础场景搭建2.1 Vue3工程创建和Cesium依赖安装项目第一步先搭好工程环境。我用Vite而不是Vue CLI因为Vite对大型依赖的预构建和热更新速度比Webpack快一大截Cesium这种体积巨大的库在Vite下启动明显更顺。npm create vuelatest # 选择组合式API、TypeScript、Router cd weather-app npm install cesiumCesium安装完成后还有个关键步骤配置静态资源路径。Cesium的Worker、图片、纹理等静态文件需要能被访问到否则会报Failed to load 0.cesium.js或RuntimeError: The Cesium CDN is not supported。Vite下我用的方式是把Cesium的静态目录拷贝到public再在入口文件设置全局CESIUM_BASE_URL。// vite.config.ts import { defineConfig } from vite; import path from path; export default defineConfig({ resolve: { alias: { : path.resolve(__dirname, src), cesium: path.resolve(__dirname, node_modules/cesium/Source), } }, });// main.ts import { CESIUM_BASE_URL } from ./config/cesium; window.CESIUM_BASE_URL CESIUM_BASE_URL;同时Cesium的类型声明也要正确引入在tsconfig.json里添加compilerOptions.types: [cesium]避免后面写代码时一堆红波浪线。实际踩坑如果你用Cesium 1.10以上的新版window.CESIUM_BASE_URL必须在使用viewer之前赋值而且不能放在懒加载文件里。我一开始图省事在组件内动态设置结果Worker始终加载不出来排查了半天才发现是时序问题。2.2 Cesium Viewer初始化和基础配置初始化Viewer是重头戏。直接new Cesium.Viewer(container)虽然能用但会把你不需要的控件全带上而且默认加载全球影像流在雨夜仿真场景里白亮的地图会很出戏。我建议通过配置项把不需要的UI统统关掉import { Viewer, Scene, Globe, Color } from cesium; const viewer new Viewer(cesiumContainer, { animation: false, baseLayerPicker: false, geocoder: false, timeline: false, sceneModePicker: false, navigationHelpButton: false, fullscreenButton: false, infoBox: false, // 核心关闭默认影像层 / 基础图层方便我们叠加自己的dark背景 baseLayer: false, }); viewer.scene.globe.enableLighting false; viewer.scene.globe.baseColor Cesium.Color.fromCssColorString(#0a0e1c); viewer.scene.skyAtmosphere.show false; viewer.scene.fog.enabled true; viewer.scene.fog.density 0.00015; viewer.scene.highDynamicRange true;这里的几点考量关闭默认影像是因为暴雨效果需要深色环境做衬底否则黑色闪电在亮色影像上几乎看不见关闭enableLighting是为了避免日照方向突然变化导致的整体偏色开启HDR是为了让闪电的光照在亮部产生真实的亮度溢出配合后面的发光后期能有更好的视觉冲击。另外视角定位很关键。初始化完成后把相机飞到目标建筑或城市上空viewer.camera.flyTo({ destination: Cesium.Cartesian3.fromDegrees(116.3913, 39.9075, 3000), orientation: { heading: Cesium.Math.toRadians(30), pitch: Cesium.Math.toRadians(-45), roll: 0, }, duration: 2, });2.3 Viewer单例管理和组件生命周期雷雨效果通常只是大屏的一个模块用户可能会在“晴天”和“暴雨”场景之间来回切换。如果每次切换都重新new ViewerCesium会不断创建新的WebGL上下文浏览器最多支持16个左右很快就卡死或者黑屏。我的方案是全局维护一个Viewer单例保存在一个独立的cesiumManager.ts中// cesiumManager.ts import { Viewer } from cesium; let viewerInstance: Viewer | null null; export function getViewer(): Viewer | null { return viewerInstance; } export function createViewer(containerId: string): Viewer { if (viewerInstance) { return viewerInstance; } viewerInstance new Viewer(containerId, { /* config */ }); return viewerInstance; } export function destroyViewer() { if (viewerInstance) { viewerInstance.destroy(); viewerInstance null; } }组件侧只负责初始化、响应式状态变更和销毁不让组件内部直接持有viewer对象这样即使路由切换底图资源也不会被反复创建销毁。这个模式在多个Vue3组件共用同一地图时尤其有用。3. 雷雨效果的核心实现3.1 乌云压顶半透明球体加着色器扰动乌云效果我一开始想用粒子系统做觉得“无数黑点聚集就是乌云”但实际做出来非常廉价颗粒感太重。后来换了思路用一个巨大的椭球体覆盖在城市上空给这个Primitive写一个自适应云层纹理的Fragment Shader用3D噪声扰动uv让云层边缘不规则内部有明暗层次整体呈半透明黑色。这里用到的核心是Cesium的MaterialAppearance配合Material自定义import { Primitive, GeometryInstance, EllipsoidGeometry, MaterialAppearance, Material } from cesium; const cloudPrimitive new Primitive({ geometryInstances: new GeometryInstance({ geometry: new EllipsoidGeometry({ radii: new Cartesian3(2500, 2500, 600), }), }), appearance: new MaterialAppearance({ material: new Material({ fabric: { type: CloudMaterial, uniforms: { uTime: 0, uNoiseTexture: noiseTexture, }, source: czm_material czm_getMaterial(czm_materialInput materialInput) { czm_material material czm_getDefaultMaterial(materialInput); vec2 st materialInput.st; // 伪3D噪声扰动云层纹理 float n snoise(vec3(st.x * 6.0, st.y * 6.0, uTime * 0.02)); material.diffuse vec3(0.02, 0.02, 0.04); material.alpha 0.85 0.15 * n; return material; } , }, }), translucent: true, }), }); viewer.scene.primitives.add(cloudPrimitive);几点细节椭球体的radii长宽比要扁看起来像横亘在天际的云层云层中心位置要在地面上方几百米处配合相机的仰视角度形成压迫感。噪声函数用单一张噪点纹理就能达到不错效果不必搞一堆程序化噪声——每一种噪声都是GPU开销。uTime应该在每一帧更新让云层有缓慢翻滚的动态。如果觉得自定义Primitive太复杂还有一个更轻量的替代方案直接在场景里叠加多个带云纹理的Billboard锚点设置在上空不同高度加透明度。这个方法代码量少一半适合临时场景或性能要求不高的页面但云层的立体感和连续性和前者没法比。3.2 雨滴粒子系统发射器、粒子纹理工笔Cesium的ParticleSystem底层是基于WebGL的GPU粒子系统初始化时配置好纹理和发射器之后每帧引擎自动更新粒子位置。这个API在官方示例里大部分是“烟花”“喷泉”这种但它完全适配雨水粒子发射方向向下生命周期短尺寸小再叠加重力加速度即可。先做一个圆形雨滴发射器。Cesium内置CircleEmitter按一个半径均匀随机生成初始位置import { ParticleSystem, CircleEmitter, Cartesian3, Color } from cesium; const rainParticles new ParticleSystem({ image: rainTexture, // 径向渐变的雨滴纹理 imageSize: new Cartesian2(8, 24), startColor: Color.fromCssColorString(rgba(160,170,200,0.6)), endColor: Color.fromCssColorString(rgba(40,50,80,0.1)), particleLife: 1.2, speed: 300, lifetime: 60, emitter: new CircleEmitter(1200), emitterModelMatrix: rainEmitterMatrix, sizeInMeters: true, updateCallback: (particle, dt) { // 重力拖拽 particle.velocity particle.velocity!.add( new Cartesian3(0, 0, -30 * dt) ); // 模拟风水平偏移 particle.velocity particle.velocity!.add( new Cartesian3(5 * dt, 2 * dt, 0) ); }, }); viewer.scene.primitives.add(rainParticles);关键参数sizeInMeters: true粒子尺寸以真实世界米为单位而不是像素这样相机拉远时雨滴会自然变小透视关系正确。startColor / endColor控制粒子在生命期内从浅蓝灰渐变为透明模拟远近距离下的雨幕层次。updateCallback每个粒子每帧都会执行很适合追加重力、风力等加速度。这里有个非常重要的位置计算发射器矩阵emitterModelMatrix必须跟随摄像机旋转否则雨是从一个固定的圆盘往下落视角一旋转雨的方向就穿帮了。我做了一个跟随相机的矩阵更新function updateRainMatrix() { const camera viewer.camera; const center Cartesian3.fromDegrees( viewer.camera.positionCartographic.longitude, viewer.camera.positionCartographic.latitude, viewer.camera.positionCartographic.height - 500 ); rainEmitterMatrix Transforms.eastNorthUpToFixedFrame(center); // 添加相机朝向的旋转偏移 rainParticles.emitterModelMatrix rainEmitterMatrix; }说句实话updateCallback里每次new Cartesian3分配对象粒子一多GC压力会比较大。优化一点的做法是把加速度向量定义为模块级常量直接复制进已有向量对象。Cesium的粒子回调里拿到的particle.velocity是Mutable版本可以直接原地修改尽量别做对象替换。3.3 闪电效果分形折线材质和随机调度闪电是整个效果里最让人眼前一亮的。实现分为两部分形状生成和渲染材质。先讲分形折线算法。闪电主干从云层中的某个坐标出发落向地面目标点中间经过几个弯折点。经典的中点位移法就可以function generateLightningPath(start: Cartesian3, end: Cartesian3, iterations: number): Cartesian3[] { const path: Cartesian3[] [start, end]; for (let i 0; i iterations; i) { const newPath: Cartesian3[] []; for (let j 0; j path.length - 1; j) { const prev path[j]; const next path[j 1]; const mid Cartesian3.lerp(prev, next, 0.5, new Cartesian3()); // 偏移向量垂直于线段方向 const dir Cartesian3.subtract(next, prev, new Cartesian3()); const perp Cartesian3.cross(dir, Cartesian3.UNIT_Z, new Cartesian3()); Cartesian3.normalize(perp, perp); const randOffset (Math.random() - 0.5) * (dir.length * 0.5); // 偏移量随层级指数递减 mid Cartesian3.add(mid, Cartesian3.multiplyByScalar(perp, randOffset, new Cartesian3()), mid); newPath.push(prev, mid); } newPath.push(path[path.length - 1]); path newPath; } return path; }注意每一级的偏移系数不能太大否则生成的就是一团乱码我用0.5作为基础偏移率迭代层级越高线段越短偏移量越小闪电看起来就越自然。渲染方面单独绘制的Entity折线颜色单一没办法表现出闪电“内芯发白、外围泛蓝”的感觉。我用的是PolylineCollection每条折线添加两层第一层宽度6像素、颜色高亮白色第二层宽度12像素、颜色半透明蓝色。视觉上外层蓝晕包裹内芯白光和真实闪电的反差很像。import { PolylineCollection, Polyline, Cartesian3, Color, PolylineDashMaterialProperty } from cesium; const lightningPolylines viewer.scene.primitives.add(new PolylineCollection()); function drawLightning(path: Cartesian3[], duration 150) { // 内芯 const corePoly lightningPolylines.add({ positions: path, width: 6, material: Color.WHITE.withAlpha(0.95), }); // 外层光芒 const glowPoly lightningPolylines.add({ positions: path, width: 12, material: Color.fromCssColorString(rgba(100,140,255,0.45)), }); setTimeout(() { lightningPolylines.remove(corePoly); lightningPolylines.remove(glowPoly); }, duration); }闪电触发频率不能太规律否则一眼假。我写了一个随机调度器用setTimeout递归实现“随机间隔触发”间隔在1.5到4秒之间抖动同时大闪电和小闪电成组出现比如每次触发放一个主干闪电隔200ms再加一个枝杈闪电听起来更接近真实天气。function scheduleLightning() { const delay 1500 Math.random() * 2500; lightningTimer window.setTimeout(() { triggerLightningFlash(); const pos getCloudRandomPosition(); const target getGroundRandomPosition(pos); const path generateLightningPath(pos, target, 5); drawLightning(path); scheduleLightning(); }, delay); }光有闪电线还不够“照亮整个天际”需要配合全屏闪烁。我是注册了一个自定义的PostProcessStage平时transparent全透明闪电瞬间把它设为白色半透明然后以每秒几百的速率快速衰减到0。这个闪烁必须和折线出现时间精确同步我是直接用viewer.clock.onTick监听时间在触发函数里记录时间戳下一帧计算衰减比率。3.4 环境氛围音效和场景压暗雨声和雷声能极大提升沉浸感但WebGL无法直接播放音频需要用到Web Audio API。雷声可以找一个雷声资源文件或者直接WebAudio合成一段低频加白噪声的“隆隆声”。我项目里用的是合成方案避免音频资源版权和加载问题function playThunder() { const ctx new AudioContext(); const bufferSize ctx.sampleRate * 2; const buffer ctx.createBuffer(1, bufferSize, ctx.sampleRate); const data buffer.getChannelData(0); for (let i 0; i bufferSize; i) { const t i / ctx.sampleRate; // 低频隆隆衰减 高频杂音 data[i] (Math.sin(t * 2 * Math.PI * 55) * (0.8 - t * 0.4)) (Math.random() * 2 - 1) * Math.max(0, 0.3 - t * 0.15); } const source ctx.createBufferSource(); source.buffer buffer; const gainNode ctx.createGain(); gainNode.gain.setValueAtTime(0.8, ctx.currentTime); gainNode.gain.linearRampToValueAtTime(0, ctx.currentTime 2); source.connect(gainNode).connect(ctx.destination); source.start(); }音效触发时机有个经验闪电先出现雷声延迟0.5到1.5秒到场这是光速音速差造成的天然效果千万别同步播放会很假。4. 性能优化与工程化实践4.1 粒子数量与纹理合批雨水粒子系统最影响性能的是粒子总量和每粒子每帧的CPU计算量。Cesium的ParticleSystem每个粒子都要经历发射、更新、剔除渲染的过程。粒子太少看不到雨丝太多直接掉帧。我的经验值是大屏幕1080P分辨率下发射速率emissionRate取800到1200粒子寿命1.2秒场景内活跃粒子大约1500个帧率能维持60。如果你需要在大场景下表现暴风雨密集度再翻一倍的话单纯调高emissionRate就不够了。此时要考虑用多个小发射器分管不同区域而不是一个超大发射器这样还能让每片区域雨势略有差异表现更自然。粒子纹理建议预生成、全局复用。不要每帧new材质不要动态修改每个粒子的imageCesium不支持粒子级别的贴图替换。要做雨滴大小差异靠imageSize属性给个范围值就够。4.2 requestAnimationFrame与定时器管理闪电调度、云层扰动、光照恢复这些高频更新我统一用一个RAF循环驱动。避免散落的setInterval是防止在路由切换时清理不干净导致定时器还在创建实体控制台报“entity is undefined”。Vue3的生命周期管理方式是这样let rafId: number; let lightningTimer: number; function startWeatherLoop() { const tick () { // 更新云层uniform的uTime // 更新雨滴发射器跟随相机 // 更新闪电闪光衰减 rafId requestAnimationFrame(tick); }; tick(); } onMounted(() { createViewer(cesiumContainer); startWeatherLoop(); scheduleLightning(); }); onUnmounted(() { cancelAnimationFrame(rafId); clearTimeout(lightningTimer); destroyWeatherEntities(); });清理工作一定要做全。因为Cesium的Primitive放在场景里不是随着Vue组件销毁自动释放的必须显式调用viewer.scene.primitives.remove()。我把rainParticles、cloudPrimitive、lightningPolylines都放在一个weatherEffectManager里统一管理销毁时逐个remove。4.3 相机限制和视角优化雷雨效果在俯视视角下观感一般雨滴是垂直落下的俯视时雨丝会被压缩成一个个点。为了让效果更突出我会限制相机的最小俯仰角防止用户切到纯俯视视角const scratchCartesian new Cesium.Cartesian3(); viewer.camera.changed.addEventListener(() { const currentPitch viewer.camera.pitch; const minPitch Cesium.Math.toRadians(-75); if (currentPitch minPitch) { viewer.camera.pitch minPitch; } });同时给相机加个移动距离阈值距离太远时自动关闭粒子系统太近时增加粒子发射速率。这个做法的收益非常高——用户拉远看全局时雨丝根本没有存在感白白消耗大量GPU。我这里设置了一个简单的状态切换相机离目标中心超过4000米降水强度降为原来的30%闪烁和云层保留因为远景主要是看天际感和云层变化。5. 常见问题与排查技巧实录5.1 雨滴纹理加载失败或显示为黑色方块排查方向纹理CORS跨域。Cesium对WebGL纹理有严格的CORS限制直接以相对路径访问本地图片如果服务器没设缓存头浏览器会拒绝写入纹理。我在开发环境用Vite代理能跑通但部署到Nginx后雨滴直接变黑块。解决方式把雨滴纹理转成dataURL或Base64内嵌虽然体积大了点但彻底规避跨域问题。也可以用Canvas绘制一张径向渐变图作为纹理代码生成完全不依赖外部资源function generateRainTexture(): HTMLCanvasElement { const canvas document.createElement(canvas); canvas.width 32; canvas.height 64; const ctx canvas.getContext(2d)!; const gradient ctx.createRadialGradient(16, 16, 2, 16, 16, 16); gradient.addColorStop(0, rgba(255,255,255,1)); gradient.addColorStop(0.4, rgba(255,255,255,0.6)); gradient.addColorStop(1, rgba(255,255,255,0)); ctx.fillStyle gradient; ctx.fillRect(0, 0, 32, 64); return canvas; }5.2 闪电折线显示不出来或半透明发灰折线显示不出来的原因90%是坐标顺序问题。Cesium的折线需要height绝对高度如果你传入的是地表高度0会被地形遮挡。闪电起点在云层高度2000米终点虽然在地表但如果相机视角倾斜这条线几乎贴着地面会被后面的山体模型遮挡。我给落地点做了抬高比如地面目标点高度设为5到15米而不是完全0米。半透明发灰则大概率是渲染合批顺序问题——闪电的PolylineCollection和半透明的粒子系统在渲染队列里重叠透明度叠加导致颜色发浑。解决方式把光效类型的Primitive放在独立的primitiveCollection里并设置这个collection的show属性在闪电触发期间才为true其他时间隐藏避免常驻的半透明体干扰其他对象的深度测试。5.3 切换选项卡后无法再进入天气效果这个问题在后台管理系统比如若依Vue3里特别常见。原因一般是组件销毁时没有正确释放Cesium的viewer或者残留了未清理的定时器。尤其当你用了keep-alive缓存组件组件的onMounted只在首次挂载执行一次之后切换回来不会重新初始化而viewer可能已经被销毁了。我建议在onActivated里判断viewer是否还在onActivated(() { const viewer getViewer(); if (viewer !viewer.isDestroyed()) { startWeatherLoop(); scheduleLightning(); } });另一个低级但高频的错误在main.ts里调用了createViewer初始化但页面里没有对应的DOM容器报Cesium widget container is undefined。这种情况通常发生在异步路由切换时页面先加载了组件但容器还没渲染好初始化完viewer换个地方挂载报错。解决方式是等nextTick()后再创建viewer。5.4 粒子系统在部分机型上掉帧严重不同显卡对粒子系统的处理性能差异很大。集显笔记本上1500个粒子后处理特效确实容易掉到30帧。优化策略按优先级优先关闭全屏抗锯齿Cesium的viewer.scene.postProcessStages.fxaa.enabled false雨丝本身是细线条锯齿比实体物体轻得多。调低粒子的imageSize小尺寸粒子填充率低大幅省GPU带宽。降低后处理分辨率。Cesium的后处理默认全分辨率改成half sizeconst flashStage viewer.scene.postProcessStages.add(new PostProcessStage({ fragmentShader: flashShader, uniforms: { uAlpha: 0.0 }, })); flashStage.uniforms.uAlpha 0.0; // 通过viewer.scene.postProcessStages.scale控制后处理分辨率 viewer.scene.postProcessStages.scale 0.5;不过注意postProcessStages.scale会同时影响所有后处理效果建议只在雷雨模式开启时将它设为0.5切回晴天就恢复1。5.5 屋顶积水效果无法和地形贴合我在做增强真实感时加了一层地面反光效果想模拟雨天地面积水的倒影。用了一个半透明黑色圆盘贴在地面上本想用height固定在地表结果地形起伏大有些地方飘在空中有些地方埋在土里。这需要用scene.globe.getHeight(ellipsoid)实时查询地表高度或者直接用Cesium.sampleTerrainMostDetailed获取确定位置的高程然后把圆盘中心点高度修正。地形查询是异步的要配合Promise处理不能同步取高度。如果地形数据不精确导致穿模更快的替代方案是放弃贴合地形直接把积水圆盘挂在半空透明度降到0.15左右勉强能骗过眼睛。最后说点经验之谈这套效果做完以后我最大的感受是天气特效的“真实感”不在于物理精度有多高而在于各效果的节奏和层次配合。单纯下一场持续不变的暴雨视觉很快就会疲劳但如果你把闪电做成分级爆发雨势在风暴中心到来时突然增强风场改变雨滴角度整个效果就“活”了。所以我在工程里把雨量的控制、闪电调度、云层浓度都做成可调参数暴露到调试面板里切数据时直接调参不用改代码。后续如果要做台风、沙尘暴或者暴雪云层和粒子系统的骨架完全可以复用只需要换纹理和加速度方向。这大概也算三维仿真项目里最值得花时间的部分——把底层效果系统化、参数化而不是写完一个效果就堆在那里。