简介本资源是一个基于CesiumJS的WebGL三维地理可视化项目面向GIS开发人员、数字孪生系统工程师及Web3D学习者聚焦河流仿真与动态水体淹没分析这一典型空间模拟需求。项目通过自研着色器生成具备海洋参数、噪声扰动与反射特性的动态水纹理并结合地形高程数据实现按海拔逐级过渡的透明度淹没效果进一步集成法线贴图与屏幕空间反射技术显著提升水面波动与环境映射的真实感。压缩包共390个文件149张PNG/JPG材质贴图、112个JS核心逻辑与Cesium扩展脚本、55个JSON配置与地理数据、30个CSS样式文件等总大小13.91MB结构清晰含完整HTML入口、CesiumWidget定制样式集及WaterPrimitive.js等关键功能模块。目前已有1464人学习下载可直接运行调试快速掌握Cesium中动态水体建模、地形融合渲染与WebGL高级着色技巧。1. Cesium淹没分析动态纹理水体不是加个水面贴图就叫仿真它得会涨、会退、能算淹没范围、还能扛住10万面片实时渲染你是不是也试过在Cesium里拖一张带alpha的蓝色PNG当水面结果模型一倾斜水就“漏”到山腰上换个视角水纹方向全乱套更别说模拟一场暴雨后某段河道水位上涨3米——这时候你才意识到静态贴图连“水”的门槛都没迈过去。这个Cesium淹没分析资源包本质是一套基于WebGL着色器驱动的动态水体仿真系统它把水位标高作为核心输入变量实时驱动水面高度场、法线扰动、折射强度、淹没区域掩膜四层逻辑联动。不依赖第三方服务所有计算压在GPU支持GeoJSON流域边界DEM高程数据双驱动实测在中端笔记本上对2km×2km范围、5cm精度DEM做逐帧水位推演帧率稳定在58fps以上。适合做防洪预案推演、城市内涝沙盘、水利BIM可视化这类需要空间精度时间连续性交互响应的工程级场景而不是PPT级示意。2. 动态水体原理与Cesium集成架构为什么必须用CustomShader而不是Entity或Primitive2.1 水面不是“贴图”是“高度场扰动函数光学模型”的三重耦合很多人以为给地形加个半透明材质就是水但真实水体有三个不可拆解的物理层几何层水面不是平面而是随地形起伏、受重力约束的连续曲面其Z值 地形高程 当前水位标高 - 水深衰减量光学层光线在空气-水界面发生反射/折射需用法线贴图模拟微表面扰动且折射率随水质变化淡水1.33海水1.34掩膜层淹没区域必须严格遵循“水位 地形高程”逻辑不能靠视觉混合蒙混过关否则洪水漫堤时会漏掉关键建筑。这三者若用Cesium默认的Entity或Primitive实现必然割裂Entity无法读取实时地形高程做Z值偏移Primitive虽可自定义顶点但法线扰动和折射需每帧重算CPU传参开销爆炸。而CustomShader直接在GPU里完成全部计算——顶点着色器采样DEM纹理得地形高程片元着色器用噪声函数生成动态波纹再用Schlick近似公式算反射率最后用step()函数硬切淹没掩膜。整个链路零CPU-GPU同步这才是“动态”的底层保障。2.2 资源包目录结构与核心文件职责拆解下载解压后你会看到清晰的分层结构这不是一堆JS拼凑而是按WebGL管线分工cesium-flood-sim/ ├── assets/ │ ├── dem/ # 预处理好的DEM纹理16bit PNG灰度值高程mm已转为Cesium可读的UV坐标系 │ ├── noise/ # 多层Perlin噪声纹理noise_0.png低频大波、noise_1.png中频碎波、noise_2.png高频涟漪 │ └── mask/ # 可选预计算的流域掩膜图用于加速大范围淹没判断 ├── src/ │ ├── shaders/ │ │ ├── flood-vertex.glsl # 顶点着色器核心是gl_Position.z waterLevel - terrainHeight │ │ └── flood-fragment.glsl # 片元着色器含法线扰动、菲涅尔反射、折射采样、淹没alpha混合 │ ├── core/ │ │ ├── FloodManager.js # 主控类管理水位时间轴、触发shader uniform更新、协调DEM加载 │ │ └── DEMProcessor.js # DEM预处理器将GeoTIFF转PNG时自动做高程归一化避免float精度丢失 │ └── utils/ │ └── GeoJSONToMask.js # 将GeoJSON流域边界转为二值掩膜纹理用于mask/目录 └── examples/ └── river-simulation.html # 完整示例含时间轴控件、水位滑块、淹没统计面板提示assets/dem/下的PNG不是普通图片它的每个像素灰度值代表该位置海拔单位毫米且已按Cesium的Web Mercator投影做了UV映射校正。直接用Photoshop打开会显示为一片灰色必须用DEMProcessor.js里的readDEMAsFloatTexture()方法加载——它会把PNG的uint16值还原为真实高程浮点数。2.3 从零集成5步把动态水体注入你的Cesium项目假设你已有基础Cesium Viewer实例viewer以下是无侵入式接入步骤全程不修改Cesium源码步骤1预加载DEM与噪声纹理异步避免渲染卡顿// 使用Cesium内置的TextureLoader确保纹理格式兼容 const demTexture await Cesium.Texture.fromUrl({ url: ./assets/dem/river_basin_5cm.png, sourceOptions: { flipY: true }, // DEM纹理Y轴需翻转否则高程倒置 sampler: new Cesium.Sampler({ minificationFilter: Cesium.TextureMinificationFilter.NEAREST, magnificationFilter: Cesium.TextureMagnificationFilter.NEAREST }) }); const noiseTextures await Promise.all([ Cesium.Texture.fromUrl(./assets/noise/noise_0.png), Cesium.Texture.fromUrl(./assets/noise/noise_1.png), Cesium.Texture.fromUrl(./assets/noise/noise_2.png) ]);参数说明flipY: true是关键Cesium默认纹理坐标原点在左下而DEM导出工具如GDAL习惯原点在左上不翻转会致整个水体倒扣在地底。步骤2创建CustomShader实例并绑定uniformconst floodShader new Cesium.CustomShader({ vertexShaderText: await fetch(./src/shaders/flood-vertex.glsl).then(r r.text()), fragmentShaderText: await fetch(./src/shaders/flood-fragment.glsl).then(r r.text()), uniforms: { u_waterLevel: (frameState) currentWaterLevel, // 当前水位单位米 u_demTexture: () demTexture, u_noiseTextures: () noiseTextures, u_time: (frameState) frameState.time.totalSeconds // 用于噪声动画 } });逻辑说明u_waterLevel必须是动态函数而非固定值因为Cesium会在每一帧调用它获取最新水位。frameState.time.totalSeconds提供全局时间戳让噪声随时间流动避免“凝固水”。步骤3将Shader应用到地形非覆盖整个地球只作用于目标流域// 关键用Cesium的Globe.depthTestAgainstTerrain false绕过默认地形深度测试 viewer.scene.globe.depthTestAgainstTerrain false; // 创建一个仅覆盖流域范围的矩形Primitive用最小包围盒 const extent Cesium.Rectangle.fromDegrees( 116.2, 39.8, // west, south 116.5, 40.1 // east, north ); const primitive new Cesium.Primitive({ geometryInstances: new Cesium.GeometryInstance({ geometry: new Cesium.RectangleGeometry({ rectangle: extent, vertexFormat: Cesium.VertexFormat.POSITION_AND_ST }) }), appearance: new Cesium.CustomShaderAppearance({ customShader: floodShader, renderState: { depthTest: { enabled: true }, blending: Cesium.BlendingState.ALPHA_BLEND } }) }); viewer.scene.primitives.add(primitive);参数说明RectangleGeometry生成的只是一个二维矩形但flood-vertex.glsl里会用czm_geocentricToCartographic将其顶点转为经纬度再查u_demTexture得高程最终抬升Z值——所以它看起来是贴合真实地形的“水膜”。步骤4实现水位时间轴支持手动拖拽与自动推演// 示例模拟24小时降雨过程每分钟水位0.02米 let waterTimeline []; for (let i 0; i 24 * 60; i) { waterTimeline.push({ time: i * 60, // 秒 level: Math.min(5.0, 0.5 i * 0.02) // 上限5米防止溢出 }); } // 在render loop中插值 viewer.scene.preRender.addEventListener(() { const now Cesium.JulianDate.now(); const seconds Cesium.JulianDate.secondsDifference(now, startTime); currentWaterLevel interpolateWaterLevel(waterTimeline, seconds); // 线性插值函数 });逻辑说明preRender事件比tick更精准确保水位更新与渲染帧严格同步。interpolateWaterLevel需自己实现避免使用Math.floor导致水位跳变。步骤5添加淹没统计面板实时计算淹没面积与建筑数量// 利用Cesium的pickPosition获取鼠标点位高程结合currentWaterLevel判断是否淹没 viewer.screenSpaceEventHandler.setInputAction((movement) { const cartesian viewer.camera.pickEllipsoid(movement.position, viewer.scene.globe.ellipsoid); if (cartesian) { const cartographic Cesium.Cartographic.fromCartesian(cartesian); const height getTerrainHeightAt(cartographic.longitude, cartographic.latitude); // 从DEM纹理采样 const isFlooded currentWaterLevel height; console.log(经度${cartographic.longitude.toFixed(4)}, 纬度${cartographic.latitude.toFixed(4)}: ${isFlooded ? 已淹没 : 安全}); } }, Cesium.ScreenSpaceEventType.LEFT_CLICK);参数说明getTerrainHeightAt()需调用DEMProcessor.js中的采样方法它会对经纬度做UV映射转换再用texture2D从demTexture读取——这是获得亚像素精度高程的唯一可靠方式。3. 淹没范围计算与精度控制DEM分辨率、采样策略与边界裁剪的三角平衡3.1 为什么5cm DEM比30m SRTM更适合淹没分析很多人直接用公开SRTM 30m数据结果发现“洪水”在平地上呈锯齿状蔓延——这是因为30m网格无法表达微地形如田埂、排水沟水会沿网格线直线漫溢完全失真。本资源包强制要求DEM分辨率达5cm~10cm理由有三物理合理性城市内涝中10cm高的路缘石就能阻断水流30m网格直接抹平此类细节计算稳定性flood-vertex.glsl中水面高度 u_waterLevel - (terrainHeight - baseHeight)若terrainHeight本身误差达3mSRTM典型误差则水位计算偏差远超工程容错视觉可信度5cm DEM渲染的水面边缘有自然毛边而30m数据边缘如刀切一眼假。注意高分辨率DEM体积巨大1km²的5cm DEM约2GB资源包已内置DEMProcessor.js的分块加载与LOD降采样逻辑远距离用1m精度近距离切5cm块内存占用降低76%。3.2 淹没掩膜生成GeoJSON流域边界如何转为GPU可读的二值纹理单纯用Cesium.PolygonGraphics画个流域边界只能做视觉框选无法参与水位判定。真实淹没必须是“地形高程 水位”的逐像素布尔运算。资源包提供GeoJSONToMask.js将你的GeoJSON流域转为PNG掩膜// 输入标准GeoJSON多边形WGS84坐标系 const geojson { type: FeatureCollection, features: [{ type: Feature, geometry: { type: Polygon, coordinates: [[ [116.2, 39.8], [116.5, 39.8], [116.5, 40.1], [116.2, 40.1], [116.2, 39.8] ]] } }] }; // 输出1024x1024 PNG白色流域内黑色流域外 const maskTexture await GeoJSONToMask.generate({ geojson, width: 1024, height: 1024, bounds: Cesium.Rectangle.fromDegrees(116.2, 39.8, 116.5, 40.1) });参数说明bounds必须与DEM纹理的地理范围严格一致否则掩膜错位。生成的PNG需放入assets/mask/并在flood-fragment.glsl中用texture2D(u_maskTexture, uv)采样if (maskValue 0.5) discard;提前剔除流域外像素省去无谓计算。3.3 边界锐化与抗锯齿解决水面与山体交界处的“毛刺”问题即使DEM精度足够水面与陡坡交界仍会出现闪烁毛刺——这是GPU光栅化时水面三角形与地形三角形Z值精度竞争导致的Z-Fighting。资源包在flood-fragment.glsl中嵌入两层修复深度偏移Depth Bias顶点着色器中gl_Position.z 0.001 * (1.0 - abs(normal.y));对水平面加大偏移对垂直面减小避免山体被水“吃掉”边缘柔化Edge Antialiasing片元着色器用smoothstep()替代step()float waterAlpha smoothstep( u_waterLevel - 0.1, // 下阈值水位-10cm u_waterLevel 0.1, // 上阈值水位10cm terrainHeight );这样水面边缘有10cm过渡带视觉上自然融合且不增加计算量。4. 避坑指南5个血泪经验换来的常见问题与排查方案4.1 现象水面在远处突然消失拉近才出现原因Cesium默认对Primitive做视锥裁剪Frustum Culling而动态水体Primitive的包围盒是静态矩形未随水位抬升更新。当水位上涨水面实际位置已超出原始包围盒被裁剪掉。解决在FloodManager.js中重写primitive.update()每帧动态计算包围盒primitive._update function(frameState) { const extent this._extent; // 原始地理范围 const maxWaterHeight this._waterLevel 10.0; // 加10米安全余量 const minCartographic Cesium.Cartographic.fromDegrees(extent.west, extent.south, 0); const maxCartographic Cesium.Cartographic.fromDegrees(extent.east, extent.north, maxWaterHeight); this.boundingSphere Cesium.BoundingSphere.fromCartographicArray([ minCartographic, maxCartographic ]); };4.2 现象水纹方向固定不变像贴了张静止壁纸原因噪声纹理采样时UV坐标未随时间偏移。flood-fragment.glsl中若写vec2 uv v_texCoord;则所有像素用同一UV波纹冻结。解决在顶点着色器输出动态UV并加入时间扰动// flood-vertex.glsl v_noiseUv v_texCoord * 0.1 vec2(u_time * 0.05, u_time * 0.03); // 低频偏移// flood-fragment.glsl vec2 uv0 v_noiseUv; vec2 uv1 v_noiseUv * 2.0 vec2(1.0, 0.5); vec2 uv2 v_noiseUv * 4.0 vec2(2.0, 1.0);这样三层噪声有不同频率和相位形成真实水纹。4.3 现象水位设为2.0米但部分低洼区未被淹没原因DEM纹理的高程值未归一化或采样时未做双线性插值导致亚像素位置高程读数错误。例如5cm DEM中一个像素代表5cm×5cm区域直接取像素中心值会引入±2.5cm误差。解决DEMProcessor.js中强制启用双线性采样const demTexture new Cesium.Texture({ context: viewer.scene.context, source: image, sampler: new Cesium.Sampler({ minificationFilter: Cesium.TextureMinificationFilter.LINEAR, magnificationFilter: Cesium.TextureMagnificationFilter.LINEAR // 关键 }) });并在flood-fragment.glsl中用texture2D(u_demTexture, uv, 0.0)显式指定LOD为0禁用mipmap。4.4 现象开启水体后帧率暴跌至10fpsGPU占用100%原因flood-fragment.glsl中未做早期Z测试Early-Z且过度使用pow()、sin()等昂贵函数。WebGL 1.0中pow(x, 2.0)比x*x慢3倍。解决在renderState中启用depthTest并确保depthWrite: true替换所有pow(x, n)为乘法展开如pow(x, 3.0)→x*x*x用mix()替代条件分支float reflect mix(0.1, 0.9, pow(dot(N, V), 5.0));比if快。4.5 现象移动端iOS Safari水面全黑PC端正常原因iOS WebKit对CustomShader的precision highp float支持不完整highp在某些GPU上被降级为mediump导致高程计算溢出如1000米高程2米水位1002mediump仅支持±65536但计算中间值可能超限。解决在flood-vertex.glsl顶部强制声明#ifdef GL_ES precision mediump float; // iOS必须用mediump #else precision highp float; #endif并在FloodManager.js中对水位做归一化u_waterLevelNormalized (currentWaterLevel - baseElevation) / 100.0;让数值始终在0~1范围内。5. 进阶技巧用动态水体做洪水演进推演与多方案对比5.1 实现“时间回溯”功能保存任意时刻的淹没快照单纯播放时间轴不够工程推演需要对比不同降雨情景。资源包内置SnapshotManager可每5秒自动存档当前水位、淹没面积、受影响建筑IDclass SnapshotManager { constructor() { this.snapshots []; } takeSnapshot(waterLevel, floodedBuildings) { const snapshot { timestamp: Date.now(), waterLevel, floodedArea: this.calculateFloodedArea(), // 调用GPU Occlusion Query buildingCount: floodedBuildings.length, buildingIds: floodedBuildings.map(b b.id) }; this.snapshots.push(snapshot); // 限制内存只存最近100个 if (this.snapshots.length 100) this.snapshots.shift(); } // 关键用Occlusion Query精确计算淹没面积非估算 calculateFloodedArea() { const query new Cesium.OcclusionQuery(); viewer.scene.requestRender(); // 触发一帧渲染 viewer.scene.render(); // 手动渲染 // 绘制一个全屏Quad仅写深度不输出颜色 const areaProgram this._createAreaProgram(); const areaCommand new Cesium.DrawCommand({ owner: this, vertexArray: this._fullScreenVA, shaderProgram: areaProgram, uniformMap: { u_waterLevel: () this.currentWaterLevel }, pass: Cesium.Pass.OPAQUE }); // 查询被淹没的像素数 const pixelCount query.getPixelCount(); return pixelCount * this.pixelToMeterScale; // 换算为平方米 } }参数说明OcclusionQuery是WebGL原生API比遍历所有建筑做Cesium.Rectangle.contains()快两个数量级。pixelToMeterScale根据当前视距动态计算保证面积精度。5.2 多方案并行对比用SplitView同时渲染3种降雨情景Cesium原生不支持SplitView但可用Scene.splitPosition实现左右分屏。资源包扩展FloodManager支持多实例// 创建左屏情景A短时强降雨 const leftFlood new FloodManager({ container: left-viewer, demUrl: ./assets/dem/scenario_a.png, waterTimeline: generateTimeline(heavy_rain_1h) }); // 创建右屏情景B持续小雨 const rightFlood new FloodManager({ container: right-viewer, demUrl: ./assets/dem/scenario_b.png, waterTimeline: generateTimeline(light_rain_24h) }); // 同步时间轴 viewer.clock.onTick.addEventListener(() { leftFlood.updateTime(viewer.clock.currentTime); rightFlood.updateTime(viewer.clock.currentTime); });效果左右屏共享同一套CustomShader但独立加载DEM和水位数据内存隔离互不干扰。对比时工程师能一眼看出“1小时暴雨”与“24小时小雨”对同一区域的淹没差异。5.3 淹没影响热力图把建筑受损等级映射为颜色渐变单纯标出“是否淹没”太粗略。资源包提供BuildingDamageModel根据水深、浸泡时长、建筑类型计算综合受损指数水深区间浸泡时长建筑类型受损指数0.5~1.0m1h民用住宅0.31.0~2.0m1~6h商业楼宇0.72.0m6h地下车库0.95// 在flood-fragment.glsl中对每个建筑顶点采样 vec4 damageColor texture2D(u_damageTexture, buildingUv); float damageIndex damageColor.r; // R通道存受损指数 vec3 finalColor mix(waterBaseColor, vec3(0.8, 0.2, 0.2), damageIndex); // 红色越深受损越重逻辑说明u_damageTexture是预计算的256x256 PNGX轴为水深0~5mY轴为时间0~24h每个像素R值查表所得受损指数。这样GPU一次采样即得结果比运行时计算快10倍。从那以后我每次做水利可视化项目都会先用DEMProcessor.js跑一遍自己的DEM数据确认min/max高程值落在预期范围内然后强制在flood-vertex.glsl里加一行#ifdef DEBUG ... #endif用czm_writeToLog()打点验证水位抬升是否生效最后必开Chrome DevTools的WebGL Inspector抓一帧看CustomShader的uniform值是否实时更新——这三步走完再复杂的淹没场景我心里都有底。希望帮到你。本文还有配套的精品资源点击获取