Cesium进阶:基于后处理与Shader实现稳定的全屏天气特效

📅 2026/6/17 10:52:02
Cesium进阶:基于后处理与Shader实现稳定的全屏天气特效
1. 为什么传统粒子系统在天气效果中不够稳定在Cesium中实现天气效果官方推荐的方式是使用粒子系统。这种方法看似简单直接但实际开发中会遇到一个致命问题当用户调整视角比如拉近、拉远或旋转场景时粒子效果会出现明显抖动甚至突然消失。这种情况在三维地理场景中尤为突出因为用户会频繁操作视角来查看不同区域。传统粒子系统的局限性主要体现在三个方面。首先是视角依赖性问题粒子位置是基于世界坐标系计算的当相机移动时每个粒子都需要重新计算位置这就导致视觉效果不连贯。其次是性能消耗大每个雨滴或雪花都是一个独立粒子当需要表现密集天气时粒子数量会急剧增加。最后是可控性差想要调整整体效果如下雪强度或雨滴倾斜角度时需要逐个修改粒子参数。我曾在项目中遇到过这样的案例当用户快速旋转地球时原本密集的雪景突然变成了局部小雪这种割裂感严重影响了用户体验。后来测试发现当相机高度超过5000米时约30%的粒子会异常消失。这就是典型的视角依赖问题。2. 后处理技术与Shader的完美组合经过多次尝试我发现用PostProcessStage配合自定义Fragment Shader才是更优解。这种方法的核心思想是把天气效果作为全屏后处理特效而不是创建无数个独立粒子。这就好比在镜头前加滤镜而不是在场景里撒面粉。后处理技术的优势非常明显。首先是稳定性无论视角如何变化特效都能完整覆盖整个屏幕。其次是性能无论场景复杂度如何后处理只需要执行一次全屏渲染。最后是灵活性通过Shader可以精确控制每个像素的表现形式。这里有个生活化的类比传统粒子系统就像在房间里真实撒纸屑而后处理方案则是用投影仪在墙上播放纸屑动画。前者需要处理每片纸屑的物理运动后者只需要一张动态贴图。3. 手把手实现雪景特效让我们以雪景为例看看具体实现步骤。首先需要创建PostProcessStage实例这是Cesium提供的后处理容器class SnowEffect { constructor(viewer, options) { this.snowStage new Cesium.PostProcessStage({ name: czm_snow, fragmentShader: this.snowShader(), uniforms: { snowSize: () this.snowSize, snowSpeed: () this.snowSpeed } }); viewer.scene.postProcessStages.add(this.snowStage); } }关键在Fragment Shader的实现。下面这段代码通过噪声算法模拟雪花飘落float snow(vec2 uv, float scale) { float time czm_frameNumber / snowSpeed; float w smoothstep(1., 0., -uv.y * (scale/10.)); if(w .1) return 0.; uv time / scale; uv.y time * 2. / scale; uv.x sin(uv.y time * .5) / scale; uv * scale; vec2 s floor(uv), f fract(uv), p; float k 3., d; p .5 .35 * sin(11. * fract(sin((spscale)*mat2(7,3,6,5))*5.)) - f; d length(p); k min(d, k); return k * w; }这个算法有几个精妙之处通过czm_frameNumber获取渲染帧数来实现动画效果用smoothstep控制雪花密度随高度变化使用噪声函数生成随机分布。你可以通过调整snowSize和snowSpeed这两个uniform变量来控制雪的大小和速度。4. 更逼真的雨天效果实现雨天效果相比雪景需要额外考虑雨滴的倾斜角度和拖尾效果。以下是RainEffect的核心代码class RainEffect { constructor(viewer, options) { this.rainStage new Cesium.PostProcessStage({ name: czm_rain, fragmentShader: this.rainShader(), uniforms: { tiltAngle: () this.tiltAngle, rainSize: () this.rainSize, rainSpeed: () this.rainSpeed } }); } }对应的Shader重点在于实现雨滴的斜向运动float hash(float x) { return fract(sin(x * 133.3) * 13.13); } void main() { float time czm_frameNumber / rainSpeed; vec2 uv (gl_FragCoord.xy * 2. - resolution.xy) / min(resolution.x, resolution.y); // 应用倾斜变换 float a tiltAngle; float si sin(a), co cos(a); uv * mat2(co, -si, si, co); uv * length(uv vec2(0, 4.9)) * rainSize 1.; float v 1. - sin(hash(floor(uv.x * 100.)) * 2.); float b clamp(abs(sin(20. * time * v uv.y * (5. / (2. v)))) - .95, 0., 1.) * 20.; gl_FragColor mix(texture2D(colorTexture, v_textureCoordinates), vec4(c, 1), .5); }这里用到了几个关键技巧hash函数生成伪随机数来分布雨滴mat2旋转矩阵实现统一的角度倾斜通过clamp和sin函数组合创建雨滴的拖尾效果。实际项目中我建议将tiltAngle设为-0.6到0.6之间的值这样能模拟不同风向的降雨效果。5. 雾效实现的特殊处理雾效与其他天气不同需要深度信息来实现距离衰减。下面是FogEffect的关键实现class FogEffect { constructor(viewer, options) { this.fogStage new Cesium.PostProcessStage({ name: czm_fog, fragmentShader: this.fogShader(), uniforms: { visibility: () this.visibility, fogColor: () this.color } }); } }对应的Shader需要读取深度纹理void main() { vec4 origcolor texture2D(colorTexture, v_textureCoordinates); float depth czm_readDepth(depthTexture, v_textureCoordinates); vec4 depthcolor texture2D(depthTexture, v_textureCoordinates); float f visibility * (depthcolor.r - 0.3) / 0.2; f clamp(f, 0.0, 1.0); gl_FragColor mix(origcolor, fogColor, f); }这里有几个注意事项必须启用深度纹理viewer.scene.postProcessStages.fxaa.enabled truevisibility参数控制雾的浓度建议范围0.1到0.5fogColor建议使用带透明度的颜色如new Cesium.Color(0.8, 0.8, 0.8, 0.5)。在高原地区展示时适当降低visibility值可以营造出更真实的海拔雾效果。6. 性能优化与常见问题解决在实际项目中我总结了几个性能优化技巧。首先是合理设置uniform变量比如snowSpeed值越大性能消耗越小但动画会变慢需要找到平衡点。其次是控制后处理阶段的数量当需要同时展示多种天气时应该合并Shader而不是叠加多个PostProcessStage。常见问题之一是边缘闪烁这通常是由于uv坐标计算不精确导致的。解决方法是在Shader开头加入vec2 uv (gl_FragCoord.xy - 0.5) / min(resolution.x, resolution.y);另一个问题是移动端兼容性。部分低端设备可能不支持高精度Shader这时需要简化算法比如将snow函数中的多层噪声减少到3-4层。可以通过czm_sceneMode判断运行环境动态调整效果精度。内存管理也很重要。当天气效果不再需要时应该及时调用destroy()方法destroy() { this.viewer.scene.postProcessStages.remove(this.snowStage); this.snowStage.destroy(); }7. 动态切换与效果组合在实际应用中我们经常需要动态切换天气效果。这里分享一个实用技巧通过统一的管理类来协调多个天气效果class WeatherManager { constructor(viewer) { this.effects { snow: new SnowEffect(viewer), rain: new RainEffect(viewer), fog: new FogEffect(viewer) }; } setWeather(type, options) { this.clearAll(); this.effects[type].show(true); if(options) this.effects[type].setOptions(options); } clearAll() { Object.values(this.effects).forEach(effect effect.show(false)); } }更高级的用法是组合多个效果比如雨雾天气。这时需要注意渲染顺序通常应该先渲染雨雪再渲染雾效。可以通过调整PostProcessStage的排序来实现viewer.scene.postProcessStages.add(rainStage); viewer.scene.postProcessStages.add(fogStage);在Shader层面也可以通过权重混合来实现效果叠加。比如在雨天基础上添加薄雾vec3 weatherEffect rainEffect * 0.7 fogEffect * 0.3;8. 进阶技巧与地形和建筑的交互要让天气效果更真实可以考虑与场景物体的交互。比如雪在地面堆积的效果可以通过读取深度信息来实现float groundSnow smoothstep(0.3, 0.5, depth); finalSnow * groundSnow;对于建筑表面的雨滴效果可以结合法线信息vec3 normal czm_getWgs84EllipsoidNormal(positionWC); float verticalFactor abs(dot(normal, vec3(0,0,1))); rainAmount * verticalFactor;这些进阶效果需要额外获取场景信息可能会影响性能建议根据实际需求选择性实现。我在一个城市级三维项目中通过LOD技术动态调整天气精度在保证效果的同时维持了60fps的流畅度。