Cesium GLSL材质实战:构建动态雷达扫描效果

张开发
2026/6/2 5:49:01 15 分钟阅读
Cesium GLSL材质实战:构建动态雷达扫描效果
1. 从零理解Cesium雷达扫描效果第一次看到Cesium实现的雷达扫描效果时那种从顶点垂直向下扩散的波纹确实让人眼前一亮。这种效果在地理信息系统中特别实用比如可以用来模拟气象雷达的探测范围或者展示某个区域的监控覆盖情况。与传统的平面雷达图不同这种立体效果能更直观地呈现空间关系。实现这个效果的核心在于两点几何体构建和材质渲染。几何体决定了雷达波的形状和位置而材质渲染则负责让这个几何体活起来。在Cesium中我们通常会选择Primitive API来实现这种自定义效果因为它比Entity API更底层性能更好也更能满足我们的定制需求。这里有个小技巧分享为什么选择圆柱体(CylinderGeometry)而不是圆锥体虽然最终效果看起来像个圆锥但从实现角度来说圆柱体有两个优势一是顶部半径可以设为0来模拟圆锥尖二是圆柱体的纹理坐标计算更规整便于后续的波纹效果实现。这个选择让我想起小时候用纸卷成圆锥形望远镜的经历 - 看似简单实则暗藏玄机。2. 构建雷达波几何体2.1 初始化场景与相机在开始编码前我们需要先搭建好舞台。假设我们的雷达位于东经110度、北纬26度的位置高度40000米。这个高度不是随便选的 - 它既要足够高以显示完整的扫描效果又不能太高导致细节看不清。let length 40000; let centerOnEllipsoid Cesium.Cartesian3.fromDegrees(110, 26, length * 0.5); viewer.camera.flyToBoundingSphere(new Cesium.BoundingSphere(centerOnEllipsoid, length));这段代码做了三件事定义了雷达的高度(length)创建了一个位于雷达中心点的笛卡尔坐标让相机飞到能完整看到雷达效果的位置我特别喜欢flyToBoundingSphere这个方法它让视角过渡非常自然就像无人机飞向目标点一样。在实际项目中你可以根据需要调整飞行时间和路径。2.2 创建圆柱几何体接下来是构建雷达波的主体 - 圆柱几何体let geometry new Cesium.CylinderGeometry({ length: length, topRadius: 0.0, // 顶部半径为0形成尖锥效果 bottomRadius: length * 0.3, // 底部半径是高度的30% vertexFormat: Cesium.MaterialAppearance.MaterialSupport.TEXTURED.vertexFormat });这里有几个关键参数需要注意topRadius设为0这样圆柱就变成了圆锥bottomRadius设为高度的30%这个比例经过多次测试视觉效果最佳vertexFormat必须设置为支持纹理的格式否则后续的材质效果无法实现我曾经尝试过不同的底部半径比例发现小于20%会让波纹太密集大于40%又显得太稀疏。30%是个不错的平衡点。2.3 计算模型矩阵为了让雷达波正确地站立在地球表面我们需要计算一个模型矩阵let modelMatrix Cesium.Matrix4.multiplyByTranslation( Cesium.Transforms.eastNorthUpToFixedFrame(Cesium.Cartesian3.fromDegrees(110, 26)), new Cesium.Cartesian3(0.0, 0.0, length * 0.5), new Cesium.Matrix4() );这段代码做了两件事使用eastNorthUpToFixedFrame创建一个以雷达位置为中心的局部坐标系将这个坐标系沿Z轴平移半个雷达高度的距离确保雷达波的底部正好接触地面这个步骤特别重要如果矩阵计算错误雷达波可能会倒在地上或者飘在空中。我第一次实现时就犯了这个错误结果雷达波横着出现在场景中活像个躺倒的电线杆。3. GLSL材质实现波纹效果3.1 理解材质的基本结构GLSL(OpenGL Shading Language)是实现各种炫酷效果的关键。在Cesium中我们可以通过Material的fabric属性来自定义GLSL材质。先来看一个基础结构material: new Cesium.Material({ fabric: { uniforms: { color: new Cesium.Color.fromCssColorString(rgba(238, 85, 34, 1)), rep: 15, offset: 0, thickness: 0.8 }, source: ...GLSL代码... } })这里定义了4个uniform变量color波纹的颜色使用RGBA格式rep波纹重复的次数值越大波纹越密集offset波纹的偏移量用于实现动画效果thickness波纹的粗细程度0-1之间的小数3.2 剖析GLSL着色器代码现在来看核心的GLSL代码czm_material czm_getMaterial(czm_materialInput materialInput) { czm_material material czm_getDefaultMaterial(materialInput); vec2 st materialInput.st; // 计算当前点到中心的距离 float dis distance(st, vec2(0.5)); // 使用取模运算创建重复的波纹 float m mod(dis offset, 1.0/rep); // 使用步进函数控制波纹显示 float a step(1.0/rep*(1.0-thickness), m); material.diffuse color.rgb; material.alpha a * color.a * dis * 1.2; return material; }这段代码的工作原理可以类比往池塘里扔石头distance计算相当于测量水面上某点到石头落点的距离mod函数就像水波的周期性扩散step函数决定了哪些区域显示波纹类似水波的波峰特别要注意的是materialInput.st它表示当前片元的纹理坐标范围是[0,1]。这个坐标系的(0.5,0.5)点就是圆锥的顶点位置。3.3 调试GLSL的小技巧调试GLSL代码可能会让人抓狂因为没有console.log可用。我常用的调试方法是把中间变量赋给最终颜色比如material.diffuse vec3(dis)可以可视化距离使用smoothstep代替step可以让边缘更柔和适当调整dis * 1.2这个系数可以控制波纹的衰减速度有一次我忘记乘以color.a结果波纹完全不透明整个圆锥变成了实心柱体。这种小错误往往最难发现所以建议每次只修改一个参数逐步调试。4. 实现动态扫描效果4.1 使用preUpdate事件驱动动画静态的波纹还不够酷我们需要让它动起来。Cesium提供了preUpdate事件在每一帧渲染前都会触发非常适合用来做动画viewer.scene.preUpdate.addEventListener(function() { let offset radar.appearance.material.uniforms.offset; offset - 0.001; // 控制波纹移动速度 if (offset 1.0) { offset 0.0; // 循环动画 } radar.appearance.material.uniforms.offset offset; });这个逻辑很简单每一帧稍微改变offset的值波纹就会移动。当offset超过1时重置为0实现循环动画。4.2 优化动画性能虽然这个效果看起来不复杂但在大规模场景中仍需注意性能避免在preUpdate中做复杂计算动画速度(0.001)不宜过快否则在低端设备上可能卡顿可以考虑使用requestAnimationFrame的deltaTime来保证动画速度一致我曾经在一个项目中同时渲染了50个雷达扫描效果导致帧率骤降。后来发现是因为每个雷达都有自己的preUpdate监听器。解决方案是使用一个全局的监听器来更新所有雷达的uniform。4.3 进阶效果扩展基础效果实现后还可以尝试更多变种添加渐变颜色修改GLSL代码让颜色随距离变化实现扇形扫描修改距离计算方式加入角度判断添加脉冲效果使用sin函数让波纹强度周期性变化比如要实现颜色渐变可以这样修改material.diffuse mix(color.rgb, vec3(1.0), dis * 2.0);这行代码会让波纹从中心到边缘逐渐变白效果更加立体。

更多文章