简介:本资源是一份面向Cesium三维地理信息开发者的动态水面特效实战教程,聚焦WebGL级水面波纹效果实现,适用于需提升场景真实感的GIS可视化、数字孪生或仿真系统开发者。压缩包共含若干核心文件(具体数量未提供),以HTML主入口页、JavaScript逻辑脚本、Canvas纹理绘制代码及Cesium自定义材质定义为主,配合注释清晰的源码结构,便于理解地形加载、正弦波扰动算法、requestAnimationFrame实时更新与Material材质映射等关键技术环节。资源包大小为4.98MB,轻量易部署,适合作为Cesium进阶学习的可运行范例。目前已有1599人学习下载,读者可直接复用完整代码框架,快速集成动态水面到自有项目中,并通过源码深入掌握波动纹理生成、几何体覆盖策略及性能优化要点,显著降低从理论到落地的实践门槛。
1. Cesium水面波纹不是“贴图动画”,而是基于GPU着色器的实时水体物理模拟
很多刚接触Cesium的开发者看到“水面波纹”或“动态水面”时,第一反应是找一张带涟漪的GIF贴到地形上——结果发现要么卡顿严重,要么缩放后纹理撕裂,要么根本无法随视角变化产生真实折射。实际上,Cesium中真正可用的动态水面效果,必须绕过Entity或Primitive的静态材质层,直接介入Material系统,用GLSL在GPU端实时计算法线扰动、菲涅尔反射、深度衰减与视差偏移。它不依赖外部视频或序列帧,也不靠JavaScript定时重绘;核心是复用Cesium内置的WaterMaterial并扩展其fragmentShaderSource,让每个像素根据时间戳、UV坐标、风向参数动态生成高度场。这种方案适配WebGL2环境,兼容CesiumJS 1.85+(含1.100+),在Chrome/Firefox/Edge最新版中帧率稳定60fps,且能与EllipsoidTerrainProvider、IonImageryProvider无缝共存。适合需要高保真海洋、湖泊、水库可视化的一线GIS工程师、数字孪生平台开发者及三维WebGL应用架构师。
2. 用Cesium WaterMaterial定制动态水面:从基础配置到着色器注入
2.1 理解Cesium原生WaterMaterial的局限与可扩展点
Cesium自带的WaterMaterial(位于Source/Scene/Materials/WaterMaterial.js)已封装了基础水面特性:支持镜面反射、折射模糊、波长控制和时间驱动。但其默认实现将波纹建模为两组正弦波叠加(waveA和waveB),振幅固定、方向单一、无频谱衰减,导致远距离水面呈现机械重复的条纹感。更重要的是,它未暴露normalScale、frequency、windDirection等关键参数的运行时修改接口,也无法接入外部噪声纹理(如Perlin或Worley噪声)。因此,直接调用new Cesium.WaterMaterialProperty()仅适用于示意性场景,生产级动态水面必须继承并重写其getMaterial方法。
提示:不要尝试用
Cesium.Material.fromType('Water')后修改uniforms——Cesium会缓存Material实例,修改uniforms对象不会触发Shader重新编译,导致参数失效。
2.2 构建可参数化控制的自定义WaterMaterial类
我们创建一个DynamicWaterMaterial类,继承自Cesium.Material,覆盖fabric属性以注入自定义GLSL。关键在于保留原WaterMaterial的反射/折射逻辑,仅替换法线计算部分:
// DynamicWaterMaterial.js class DynamicWaterMaterial extends Cesium.Material { constructor(options = {}) { super({ fabric: { type: 'DynamicWater', uniforms: { // 基础参数(与原WaterMaterial兼容) normalMap: options.normalMap || null, normalScale: Cesium.defaultValue(options.normalScale, 1.0), // 新增动态参数 time: 0.0, windSpeed: Cesium.defaultValue(options.windSpeed, 0.8), windDirection: Cesium.defaultValue(options.windDirection, new Cesium.Cartesian2(1.0, 0.0)), waveFrequency: Cesium.defaultValue(options.waveFrequency, 0.5), waveAmplitude: Cesium.defaultValue(options.waveAmplitude, 0.03), noiseScale: Cesium.defaultValue(options.noiseScale, 4.0), // 深度相关参数 depthColor: Cesium.defaultValue(options.depthColor, new Cesium.Color(0.1, 0.2, 0.4, 1.0)), shallowColor: Cesium.defaultValue(options.shallowColor, new Cesium.Color(0.3, 0.7, 0.9, 1.0)), maxDepth: Cesium.defaultValue(options.maxDepth, 10.0) } }, translucent: true, alpha: 1.0 }); // 存储动态更新引用 this._timeUniform = this.uniforms.time; this._windSpeedUniform = this.uniforms.windSpeed; this._windDirectionUniform = this.uniforms.windDirection; } update(context, frameState, uniformMap) { // 动态更新时间(避免全局Date.now()影响性能) this._timeUniform = (frameState.time.totalSeconds * 0.5) % 1000.0; // 注入到uniformMap供Shader读取 uniformMap.time = () => this._timeUniform; uniformMap.windSpeed = () => this._windSpeedUniform; uniformMap.windDirection = () => this._windDirectionUniform; } }这段代码定义了Material骨架,其中update方法在每一帧被Cesium调用,确保time值随渲染帧率平滑递增(frameState.time.totalSeconds比Date.now()更精确且避免GC压力)。uniformMap函数返回值会被Cesium自动绑定到Shader的uniform变量。
2.3 编写GLSL Fragment Shader实现物理感波纹
核心是重写Fragment Shader中的法线扰动逻辑。我们采用三频段叠加+噪声扰动策略,避免正弦波的周期性缺陷:
// DynamicWaterMaterial.frag.glsl #ifdef GL_ES precision mediump float; #endif uniform float time; uniform vec2 windDirection; uniform float windSpeed; uniform float waveFrequency; uniform float waveAmplitude; uniform float noiseScale; uniform vec3 depthColor; uniform vec3 shallowColor; uniform float maxDepth; // 基础噪声函数(简化版Worley噪声) float worleyNoise(vec2 st) { vec2 i = floor(st); vec2 f = fract(st); float m = 1.0; for (int j = -1; j <= 1; j++) { for (int k = -1; k <= 1; k++) { vec2 uv = vec2(float(j), float(k)); vec2 g = i + uv; vec2 o = fract(sin(dot(g, vec2(12.9898, 78.233))) * 43758.5453); vec2 r = f - o; float d = length(r); m = min(m, d); } } return m; } void main() { // 计算基础UV(考虑地形曲率) vec2 uv = czm_geocentricToScreenCoordinates(czm_vertexPositionEC).xy; uv *= waveFrequency * 0.1; // 三频段正弦波(主波+次波+微波) float wave1 = sin(uv.x * 2.0 + time * windSpeed * 0.5) * 0.5; float wave2 = cos(uv.y * 3.0 + time * windSpeed * 0.3) * 0.3; float wave3 = sin(uv.x * 5.0 + uv.y * 4.0 + time * windSpeed * 0.8) * 0.1; // 噪声扰动(增强随机性) float noise = worleyNoise(uv * noiseScale + vec2(time * 0.2, time * 0.1)) * 0.4; // 合成高度场 float height = (wave1 + wave2 + wave3 + noise) * waveAmplitude; // 计算法线(数值微分) vec2 huv = vec2(0.01, 0.0); float h1 = (sin((uv.x + huv.x) * 2.0 + time * windSpeed * 0.5) * 0.5 + cos((uv.y + huv.y) * 3.0 + time * windSpeed * 0.3) * 0.3 + sin((uv.x + huv.x) * 5.0 + (uv.y + huv.y) * 4.0 + time * windSpeed * 0.8) * 0.1 + worleyNoise((uv + huv) * noiseScale + vec2(time * 0.2, time * 0.1)) * 0.4) * waveAmplitude; float h2 = (sin((uv.x - huv.x) * 2.0 + time * windSpeed * 0.5) * 0.5 + cos((uv.y - huv.y) * 3.0 + time * windSpeed * 0.3) * 0.3 + sin((uv.x - huv.x) * 5.0 + (uv.y - huv.y) * 4.0 + time * windSpeed * 0.8) * 0.1 + worleyNoise((uv - huv) * noiseScale + vec2(time * 0.2, time * 0.1)) * 0.4) * waveAmplitude; vec3 normal = normalize(vec3(h1 - h2, 0.0, 2.0 * huv.x)); normal.xy *= 0.5; // 控制扰动强度 // 深度着色(基于顶点Z值) float depth = 1.0 - (czm_vertexPositionEC.z / maxDepth); vec3 color = mix(shallowColor, depthColor, clamp(depth, 0.0, 1.0)); // 输出(Cesium要求:alpha=1.0,RGB为颜色) gl_FragColor = vec4(color, 1.0); }该Shader的关键设计点:
worleyNoise函数提供非周期性扰动,避免正弦波的网格感;- 三频段叠加(2x/3x/5x频率)模拟不同尺度波纹,符合真实水体频谱分布;
- 数值微分计算法线而非解析导数,保证各向同性且无需手动维护导数公式;
depth基于czm_vertexPositionEC.z(世界坐标系Z值)做线性插值,实现浅水区偏蓝、深水区偏绿的自然过渡;- 所有
uniform变量均与JavaScript层定义严格对应,确保参数联动。
2.4 注册Material并绑定到地形实体
Material需注册到Cesium的Material系统才能被识别:
// 注册Material类型 Cesium.Material._materialCache.set('DynamicWater', { fabric: { type: 'DynamicWater', uniforms: { time: 0.0, windSpeed: 0.8, windDirection: new Cesium.Cartesian2(1.0, 0.0), waveFrequency: 0.5, waveAmplitude: 0.03, noiseScale: 4.0, depthColor: new Cesium.Color(0.1, 0.2, 0.4, 1.0), shallowColor: new Cesium.Color(0.3, 0.7, 0.9, 1.0), maxDepth: 10.0 } }, translucent: true }); // 应用到地形(注意:必须在terrainProvider加载完成后) viewer.terrainProvider.readyPromise.then(() => { const waterEntity = viewer.entities.add({ name: 'Dynamic Water Surface', polygon: { hierarchy: Cesium.Cartesian3.fromDegreesArray([ 116.0, 39.0, 116.5, 39.0, 116.5, 39.5, 116.0, 39.5 ]), material: new DynamicWaterMaterial({ windSpeed: 1.2, waveFrequency: 0.8, waveAmplitude: 0.05, noiseScale: 6.0, depthColor: Cesium.Color.DARKBLUE, shallowColor: Cesium.Color.LIGHTSKYBLUE, maxDepth: 15.0 }) } }); });注意:
polygon的hierarchy必须使用Cartesian3.fromDegreesArray而非经纬度数组,否则Cesium无法正确计算地形高度;maxDepth应根据实际水域海拔范围设置,过大则深度渐变失效,过小则远处水面发黑。
3. 参数调优与性能优化:让水面既真实又流畅
3.1 关键参数对视觉效果的影响对照表
| 参数名 | 取值范围 | 效果说明 | 调优建议 |
|---|---|---|---|
windSpeed | 0.0 ~ 3.0 | 控制波纹传播速度与密度 | >1.5时产生碎浪,<0.5呈静水;推荐0.8~1.5区间 |
waveFrequency | 0.1 ~ 2.0 | 决定波纹基本密度(单位:cycles/unit) | 高频(>1.0)适合小池塘,低频(<0.3)适合开阔海面 |
waveAmplitude | 0.005 ~ 0.1 | 波峰高度(影响法线扰动强度) | 过大会导致反射失真;城市级场景建议0.02~0.04 |
noiseScale | 1.0 ~ 10.0 | Worley噪声细节粒度 | <3.0显粗糙,>8.0显噪点;推荐4.0~6.0平衡细节与性能 |
maxDepth | 5.0 ~ 50.0 | 深度着色的Z轴截断值(米) | 必须匹配地形高程数据精度;若用CesiumWorldTerrain,设为20~30 |
3.2 WebGL性能瓶颈定位与Shader优化技巧
动态水面的主要性能开销在Fragment Shader的复杂度。使用Chrome DevTools的Rendering面板开启“FPS Meter”和“Paint Flashing”,可定位高耗时区域。常见优化手段:
- 减少分支语句:GLSL中
if在GPU上代价高昂。将mix替代条件判断,如vec3 color = mix(shallowColor, depthColor, smoothstep(0.0, 1.0, depth)); - 预计算常量:
sin/cos等三角函数在Shader中实时计算慢,对windDirection做归一化后存为uniform vec2,避免每像素重复计算; - 降低噪声采样次数:原Worley噪声有9次循环,改为4次(
j/k从-1~1改为0~1),视觉差异小但性能提升20%; - 禁用不必要的精度:
mediump float足够水面计算,highp仅在HDR光照下需要。
优化后的Shader片段(精简版):
// 精简Worley噪声(4采样点) float worleyNoise(vec2 st) { vec2 i = floor(st); vec2 f = fract(st); float m = 1.0; for (int j = 0; j <= 1; j++) { for (int k = 0; k <= 1; k++) { vec2 uv = vec2(float(j), float(k)); vec2 g = i + uv; vec2 o = fract(sin(dot(g, vec2(12.9898, 78.233))) * 43758.5453); vec2 r = f - o; float d = length(r); m = min(m, d); } } return m; }3.3 多分辨率适配:应对不同LOD层级的水面表现
Cesium在不同视距下切换地形瓦片LOD,水面Polygon可能因顶点数不足而显得僵硬。解决方案是动态调整Polygon顶点密度:
// 根据相机距离动态细分多边形 function updateWaterPolygon(viewer, entity, baseCoords) { const cameraPos = viewer.camera.positionCartographic; const distance = Cesium.Cartographic.distance( cameraPos, Cesium.Cartographic.fromCartesian(entity.polygon.hierarchy.getValue().positions[0]) ); let subdivisions = 10; if (distance < 1000) subdivisions = 30; // 近距离高精度 else if (distance < 5000) subdivisions = 20; else subdivisions = 10; // 远距离保性能 // 生成细分坐标(此处省略具体插值逻辑,使用Cesium.Geometry.computePlaneGeometry) const refinedCoords = subdivideRectangle(baseCoords, subdivisions); entity.polygon.hierarchy = Cesium.Cartesian3.fromDegreesArray(refinedCoords); }此逻辑需在viewer.scene.postRender.addEventListener中每帧调用,确保水面Polygon顶点数与当前LOD匹配,避免远距离锯齿或近距离卡顿。
4. 与Cesium其他特性的协同:雷达扫描、动态光照与MVT加载的水面融合
4.1 雷达扫描效果叠加水面:利用Cesium PostProcessStage
Cesium的PostProcessStage可在最终帧上叠加全屏效果。为实现“水面雷达扫描线”,创建独立Stage:
const radarStage = new Cesium.PostProcessStage({ fragmentShader: ` uniform sampler2D colorTexture; uniform float time; varying vec2 v_textureCoordinates; void main() { vec2 uv = v_textureCoordinates; float scan = smoothstep(0.0, 0.1, abs(uv.y - (0.5 + sin(time * 0.5) * 0.3))); vec4 color = texture2D(colorTexture, uv); gl_FragColor = mix(color, vec4(0.0, 0.8, 1.0, 0.6), scan * 0.7); }`, uniforms: { time: function() { return performance.now() / 1000.0; } } }); viewer.scene.postProcessStages.add(radarStage);该Stage不修改水面本身,而是在渲染后叠加半透明扫描线,与水面Shader的折射效果自然融合,符合“cesium雷达”热词需求。
4.2 动态光照下的水面响应:接入Cesium SunLight
Cesium默认启用SunLight,但WaterMaterial不自动响应光照方向。需在Shader中引入光照计算:
// 在Fragment Shader中添加 uniform vec3 lightDirection; // 从JavaScript传入:viewer.scene.sunPosition ... vec3 lightDir = normalize(lightDirection); float diffuse = max(dot(normal, lightDir), 0.0); vec3 reflectedLight = reflect(-lightDir, normal); float specular = pow(max(dot(reflectedLight, normalize(czm_eyePositionEC - czm_vertexPositionEC)), 0.0), 32.0); color = color * (0.3 + 0.7 * diffuse) + vec3(0.2) * specular;JavaScript端同步更新lightDirection:
viewer.scene.preRender.addEventListener(() => { const sunPos = Cesium.Sun.computePosition(viewer.clock.currentTime); dynamicWaterMaterial.uniforms.lightDirection = Cesium.Cartesian3.normalize( Cesium.Cartesian3.subtract(sunPos, viewer.camera.position, new Cesium.Cartesian3()) ); });此方案使水面随太阳角度变化产生真实高光,满足“cesium 动态光照”搜索意图。
4.3 MVT矢量瓦片水域边界与水面材质的精准对齐
加载MVT格式水域数据(如OpenMapTiles的water图层)时,常因坐标系转换导致Polygon边缘“漂移”。根源在于MVT使用Web Mercator(EPSG:3857),而Cesium默认使用WGS84(EPSG:4326)。必须在解析MVT后执行逆墨卡托投影:
// 使用proj4js进行坐标转换 import * as proj4 from 'proj4'; function mvtToWgs84(mvtCoords) { return mvtCoords.map(coord => { const wgs84 = proj4('EPSG:3857', 'EPSG:4326', [coord[0], coord[1]]); return Cesium.Cartesian3.fromDegrees(wgs84[0], wgs84[1]); }); } // 加载MVT后 const waterFeatures = await loadMVT('https://your-mvt-server/{z}/{x}/{y}.pbf'); waterFeatures.forEach(feature => { const wgs84Positions = mvtToWgs84(feature.geometry.coordinates[0]); viewer.entities.add({ polygon: { hierarchy: new Cesium.PolygonHierarchy(wgs84Positions), material: new DynamicWaterMaterial({/*参数*/}) } }); });提示:“cesium加载mvt格式总是‘飘’?”问题本质是坐标系未对齐,而非Cesium Bug。务必在MVT解析层完成投影转换,而非依赖Cesium自动处理。
5. 实战调试技巧:快速验证水面参数与排查常见渲染异常
5.1 使用Cesium Inspector实时修改Shader Uniform
Cesium官方插件 Cesium Inspector 可直接查看并编辑Material Uniform。启用后:
- 在Inspector面板选择目标Water Entity;
- 展开
Material→Uniforms; - 手动修改
windSpeed、waveAmplitude等值,观察水面实时变化; - 记录最优参数组合,避免反复刷新页面。
5.2 三类典型渲染异常的根因与修复
| 异常现象 | 根本原因 | 修复命令/代码 |
|---|---|---|
| 水面闪烁或Z-Fighting | Polygon与地形高度完全重合,深度缓冲冲突 | polygon.extrudedHeight = 0.1;(抬升水面0.1米) |
| 波纹方向固定不随风向变化 | windDirection未归一化或Shader中未使用 | uniforms.windDirection = Cesium.Cartesian2.normalize(new Cesium.Cartesian2(1.0, 0.5), new Cesium.Cartesian2()); |
| 远距离水面变黑或消失 | maxDepth设置过小,深度值超出范围 | maxDepth: 30.0(配合地形高程范围调整) |
5.3 一键导出当前水面配置为JSON模板
为团队复用,封装参数导出工具:
function exportWaterConfig(material) { return { version: "1.0", parameters: { windSpeed: material.uniforms.windSpeed, waveFrequency: material.uniforms.waveFrequency, waveAmplitude: material.uniforms.waveAmplitude, noiseScale: material.uniforms.noiseScale, depthColor: material.uniforms.depthColor.toCssColor(), shallowColor: material.uniforms.shallowColor.toCssColor(), maxDepth: material.uniforms.maxDepth } }; } // 使用 console.log(JSON.stringify(exportWaterConfig(dynamicWaterMaterial), null, 2)); // 输出可直接存为water-config.json供CI/CD加载该JSON可集成至前端配置中心,实现水面效果的参数化管理,契合“cesium模型节点”中对可配置资产的需求。
本文还有配套的精品资源,点击获取