很多人第一次看到别人做的 Cesium 大屏时,都会有同一个疑问:地图上那个雷达扫描圈是不是一个 GIF 贴图?飞线是不是 Cesium 官方 API 里现成的方法?等自己动手去查文档,却发现 Cesium 自带的材质只有颜色、条纹、网格这些基础类型,根本找不到“扫描”和“飞线”的入口。
这里可以给出一个明确判断:你看到的高质量地图特效,绝大多数不是前端动画拼出来的,而是在 WebGL 的片元着色器里逐像素“算”出来的。换句话说,别人做得丝滑,不是因为他调用了什么隐藏 API,而是他掌握了一个底层能力——自定义 Shader。
这篇文章会从 WebGL 渲染管线讲起,然后拆解两个最经典的特效:地图扫描(雷达扇形扫描 + 波纹扩散)和飞线动画。全程会给完整可运行的 Cesium 自定义材质代码,并解释每个 Shader 函数在干什么。读完你会理解一个核心事实:Cesium 特效的本质,是控制 GPU 在每个像素上做什么。
1. 为什么官方 API 做不出你想要的特效
先看 Cesium 官方内置了哪些材质。在 Entity 的 polygon、polyline 上,Cesium 默认提供了 Color、Image、Stripe、Grid、Checkerboard 等基础材质。这些材质能解决“贴图”问题,但都缺少同一个能力:随时间变化的动态计算。
有人会想,那我用 JavaScript 定时器不断修改 Entity 的 position 或 style,是不是也能做出扫描效果?可以,但这是典型的错误方向。原因有三点:
第一,Cesium 场景中的 Entity 数量一旦增多,CPU 提交绘制命令的开销会快速上升。如果你为了让扫描圈动起来,每帧都去修改 geometry 的坐标或材质参数,主线程会被拖垮。
第二,前端定时器的精度只能到毫秒级,而且大量 setInterval 会让帧率不稳定。扫描特效本身需要的是“逐帧输入时间”,而不是“定时触发一段逻辑”。
第三,如果你用透明度渐变、CSS 旋转、Canvas 贴图这些方式去模拟,效果是平面化的,和三维地形、倾斜摄影、模型之间没有任何遮挡关系,一眼就能看出是假的。
真正的高质量方案,是把“扫描”这个过程的计算交给 GPU。在 Cesium 中,你可以通过自定义 Material 把一段 GLSL 代码注入到渲染管线中。每一帧,GPU 会为几何体覆盖的每一个像素执行这段代码,同时 Cesium 会把当前帧号注入给 Shader。这样,动画不再依赖 JavaScript 定时器,而是完全由显卡的渲染节奏驱动。
所以,你要掌握的第一个关键点是:Cesium 特效 = 几何体轮廓 + 自定义 Shader 材质。几何体负责“画在哪里”,Shader 负责“每一帧画成什么样”。
2. WebGL 渲染管线与 Shader 核心原理
在进入代码之前,有必要把 Shader 的基础讲清楚。很多人看到 Shader 代码就害怕,其实它没有想象中复杂。
2.1 顶点着色器和片元着色器
WebGL 的渲染管线里有两个必须写的着色器。
顶点着色器(Vertex Shader)的作用是决定几何体的顶点在屏幕上哪个位置。它执行的次数等于顶点数。对一个多边形来说,顶点可能只有几十个到几百个。
片元着色器(Fragment Shader)的作用是决定每个像素最终显示成什么颜色。它执行的次数等于几何体覆盖的像素数。一块覆盖几百个像素、面积很大的多边形,片元着色器会被执行几十万甚至上百万次。
Cesium 的自定义 Material 本质上就是让你写一个“片元着色器的片段”,而不是完整的着色器。你不需要关心顶点怎么变换,也不需要自己去拿 gl_FragColor。Cesium 定义了自己的材质接口:你写一个 czm_getMaterial 函数,返回一个 czm_material 结构体,Cesium 会把它继续交给后续的光照阶段。
czm_material czm_getMaterial(czm_materialInput materialInput) { czm_material material = czm_getDefaultMaterial(materialInput); // 在这里修改 material.diffuse 和 material.alpha return material; }这段结构是所有 Cesium 自定义材质的地基。materialInput 里带着这个像素的纹理坐标、位置、法线等信息,czm_getDefaultMaterial 会返回一个默认材质对象,你只需要修改它的颜色和透明度字段。
2.2 三个高频使用的 GLSL 函数
在 Shader 里做扫描和飞线特效,离不开 mod、fract、smoothstep 这三个函数。
mod 是取模函数。它可以让一个递增的数值在某个区间内循环。Shader 动画的本质,就是把时间变成周期性信号。比如 mod(x, 2.0) 的结果永远在 0 到 2 之间循环,这就构成了“波”。
fract 是取小数部分的函数。fract(x) 返回 x 去掉整数部分后的值,结果永远在 0 到 1 之间。它比 mod 更适合做“从头到尾再回到头”的循环效果。
smoothstep 是平滑过渡函数。smoothstep(edge0, edge1, x) 会在 edge0 到 edge1 之间做一次平滑插值,返回 0 到 1 之间的值。用它来实现边缘虚化、扫描线渐变,比单纯用 if 判断要自然得多。
2.3 时间从哪来
Shader 是每一帧被执行的,它怎么知道当前时间?
Cesium 在编译材质时,会注入一组内置 uniform 变量,其中就包括 czm_frameNumber,表示当前帧号。你可以在材质代码中直接使用它,不需要声明。
float time = czm_frameNumber * 0.01;帧号是一个持续递增的整数,乘以一个很小的系数就变成了一个稳定的时间流。有了这个时间流,你就可以用它驱动角度旋转、波纹扩散、飞线流动。
2.4 一个关键认知
关于 Cesium 的自定义材质,最容易误导新手的一点是:你在 Material 里面写的代码,不是“一个完整的 Shader”,而是“材质计算函数”。所以你不能直接在代码里写 gl_FragColor,也不能直接用 attribute、varying 这些顶点着色器里的东西。你需要理解 materialInput 这个结构体里有哪些字段,以及 czm_material 需要你填哪些字段。
在实际开发里,你只需要关注两个字段:diffuse 控制颜色,alpha 控制透明度。其他的如 specular、emission 等,在特效材质里通常不做特殊处理。
3. 环境准备与基础配置
本文的示例基于 Cesium 1.100 以上的版本演示,但核心的 Material 机制在早期版本中就已经存在,基本原理一致。如果你用的是 Vue + Vite 工程,可以按下面步骤搭建一个最小环境。
3.1 初始化项目
npm create vite@latest cesium-shader-demo -- --template vanilla cd cesium-shader-demo npm install cesium安装完毕后,需要在入口文件中引入 Cesium 的样式和核心库。以原生 Vite 项目为例:
// main.js import * as Cesium from 'cesium'; import 'cesium/Build/Cesium/Widgets/widgets.css'; // 如果使用官方在线资源,需要配置 token Cesium.Ion.defaultAccessToken = '你的 token';如果你在内网环境,或者不想接入 Cesium Ion,可以自己准备离线影像和地形数据。但需要说明的是,地图瓦片和地形切片属于数据工程问题,和本文要讲的特效 Shader 没有直接关系。特效运行在白膜、影像、地形、模型之上都可以。
3.2 创建基础场景
const viewer = new Cesium.Viewer('cesiumContainer', { animation: false, timeline: false, baseLayerPicker: false, infoBox: false, selectionIndicator: false }); viewer.scene.globe.enableLighting = false;一个干净的 viewer 就创建好了。接下来的所有特效,都是在 viewer 场景中叠加几何体。
3.3 检查浏览器是否支持 WebGL
如果运行后页面黑屏,优先检查 WebGL 是否可用。在 Chrome 地址栏输入 chrome://gpu 查看 WebGL 状态,或直接在控制台执行下面代码:
const canvas = document.createElement('canvas'); const gl = canvas.getContext('webgl'); console.log(gl ? 'WebGL OK' : 'WebGL 不可用');如果控制台输出 WebGL 不可用,通常是浏览器硬件加速被关闭,或者显卡驱动太旧。在设置中开启硬件加速并重启浏览器即可。
4. 地图扫描特效:雷达扇形扫描与波纹扩散
现在开始写本文第一个完整特效。雷达扫描通常由两部分组成:一个扇形扫过的区域,以及从中心向外扩散的波纹。两部分都是通过对像素坐标的计算实现的。
4.1 特效思路拆解
先明确一个基础事实:所有基于平面坐标的特效,都是在纹理坐标 st 上做数学运算。
st 坐标的范围是 0 到 1。对于多边形来说,(0, 0) 是左下角,(1, 1) 是右上角。我们希望扫描效果从多边形中心向外扩散,所以要把 st 坐标从“左下角原点”变换到“中心原点”。
vec2 uv = st - vec2(0.5);变换后,uv 的范围大约在 -0.5 到 0.5 之间,中心点的坐标就是 (0, 0)。接下来,用 atan 计算每个像素相对于中心点的角度,用 length 计算半径。有了角度和半径,就可以组合出各种扫描效果。
4.2 雷达扫描材质代码
创建一个 radarScanMaterial.js 文件:
// radarScanMaterial.js import * as Cesium from 'cesium'; export function createRadarScanMaterial() { // 在 Cesium 的材质缓存中注册自定义材质 Cesium.Material._materialCache.addMaterial('RadarScan', { fabric: { type: 'RadarScan', uniforms: { color: Cesium.Color.fromCssColorString('#00ffaa'), scanSpeed: 2.0, sectorAngle: 0.6 }, source: ` czm_material czm_getMaterial(czm_materialInput materialInput) { czm_material material = czm_getDefaultMaterial(materialInput); vec2 st = materialInput.st; vec2 uv = st - vec2(0.5); // 1. 计算当前像素相对中心的角度和半径 float angle = atan(uv.y, uv.x); float radius = length(uv) * 2.0; // 2. 扫描角度随时间递增,周期为 2 * PI float time = czm_frameNumber * 0.01 * scanSpeed; float scanAngle = mod(time, czm_twoPi); // 3. 计算当前像素角度与扫描角度的夹角 float angleDiff = abs(scanAngle - angle); // 处理角度环绕:角度差超过 PI 时取补角 angleDiff = min(angleDiff, czm_twoPi - angleDiff); // 4. 扇形扫描:夹角小于 sectorAngle 时高亮 float sector = 1.0 - smoothstep(0.0, sectorAngle, angleDiff); // 5. 半径方向做波纹扩散,用 fract 产生循环波 float ringCount = 4.0; float wave = 1.0 - abs(fract(radius * ringCount - time * 2.0) - 0.5) * 2.0; float ring = (1.0 - wave) * 0.6; // 6. 合成颜色 vec3 scanColor = color.rgb; float alpha = sector * 0.7 + ring * sector; material.diffuse = scanColor; material.alpha = alpha; return material; } ` }, // 多边形需要关闭纹理坐标的重复采样 translucent: true }); }这段代码有几个关键点需要理解。
atan(uv.y, uv.x) 会返回该像素相对于中心点的角度范围是 -PI 到 PI。扫描角 scanAngle 由帧号驱动,范围是 0 到 2PI。直接比较两者会碰到角度环绕问题,比如扫描角是 350 度时,目标像素是 10 度,角度差计算出来是 340 度,但实际差异只有 20 度。所以代码里用 min(angleDiff, czm_twoPi - angleDiff) 把角度差折算到 0 到 PI 范围内。
波纹扩散的核心是这段:
float ringCount = 4.0; float wave = 1.0 - abs(fract(radius * ringCount - time * 2.0) - 0.5) * 2.0;fract 的参数是一个随半径增加而增加、随时间变化而变化的数值。fract 的结果在 0 到 1 之间循环,这就在半径方向上形成了多个“环”。1.0 - abs(x - 0.5) * 2.0 会把这个循环信号变成三角波。三角波的值在环的边界处是 0,在环的中心是 1。然后我们用 1.0 - wave 反转,让环变成暗底亮线。
4.3 创建扫描实体
材质注册之后,在场景中添加一个多边形即可看到效果:
// createRadarEntity.js import * as Cesium from 'cesium'; import { createRadarScanMaterial } from './radarScanMaterial.js'; export function addRadarScan(viewer, center, radius) { // 确保材质已注册 createRadarScanMaterial(); // 以中心点为中心,创建一个正六十四边形区域作为扫描底座 const positions = []; const count = 64; for (let i = 0; i <= count; i++) { const angle = (i / count) * Cesium.Math.TWO_PI; const x = center.longitude + radius * Math.cos(angle) / 100000; const y = center.latitude + radius * Math.sin(angle) / 100000; positions.push(Cesium.Cartesian3.fromDegrees(x, y)); } return viewer.entities.add({ polygon: { hierarchy: new Cesium.PolygonHierarchy(positions), material: new Cesium.Material({ fabric: { type: 'RadarScan' } }), classificationType: Cesium.ClassificationType.BOTH } }); }注意这里用了一个近似换算:把半径除以 100000 当作经纬度增量。这种写法只适合在近距离、低纬度区域演示,实际项目中建议把半径换算成经纬度差,或者改用 Cesium 的 Cartesian3 坐标计算。
运行后,你会看到一个以中心点为圆心的扇形扫描区域,同时半径方向上会有波纹从中心向外扩散。因为整个流程都在 GPU 的片元着色器中完成,持续运行时不会占用 JavaScript 主线程。
4.4 波纹扩散效果增强思路
如果觉得波纹不够明显,可以增加 ringCount 的数值,或者提高 time 前的系数。如果觉得扫描扇形边缘太锐利,可以提高 smoothstep 的第二个参数值,让过渡更柔和。
一个可以继续深入的方向是:把扫描效果从“贴在地面”改成“垂直扫描面”,用于山体或建筑的可视域分析。思路本质相同,差别在于把 st 坐标换成垂直平面上的坐标。Cesium 的可视域分析、天际线分析,底层用的也是类似的渲染技巧。
5. 飞线动画实战:PolylineTrail 材质
飞线是另一个高频特效。它看起来像是带有渐变尾迹的光线从 A 点飞到 B 点,常用于人口迁徙、物流流向、航线分布等可视化场景。
5.1 飞线的核心原理
飞行线的几何体是折线。折线本身是静态的,要让“飞行”的感觉出现,需要在 Shader 里沿着折线方向移动一个高亮区域。
折线材质有一个特殊的优势:materialInput.st 中,st.s 通常代表沿折线的进度,st.t 代表垂直于折线的方向。这意味着我们只需要根据 st.s 的值来控制亮度,就能得到沿折线移动的效果。
飞线尾迹的核心算法是:
float progress = fract(st.s - time);st.s 是 0 到 1 的进度值,减去随时间递增的时间值,再用 fract 取小数部分,就得到了一个从 0 到 1 周期性循环的进度。然后计算当前片元在这个循环中的位置,尾迹处亮度高,前端之外透明度为 0。
5.2 飞线材质代码
创建 trailLineMaterial.js 文件:
// trailLineMaterial.js import * as Cesium from 'cesium'; export function createTrailLineMaterial() { Cesium.Material._materialCache.addMaterial('PolylineTrail', { fabric: { type: 'PolylineTrail', uniforms: { color: Cesium.Color.fromCssColorString('#00e5ff'), trailLength: 0.3, speed: 1.0 }, source: ` czm_material czm_getMaterial(czm_materialInput materialInput) { czm_material material = czm_getDefaultMaterial(materialInput); // 获取沿线进度 float progress = materialInput.st.s; // 用时间驱动进度,fract 保证循环 float time = czm_frameNumber * 0.01 * speed; float tail = fract(progress - time); // 头部亮,尾部逐渐变暗 float gradient = smoothstep(0.0, trailLength, tail); // 超出尾迹范围的部分透明 float alpha = smoothstep(trailLength, 0.0, tail); // 让飞线在垂直方向上有一个高光核心 float core = 1.0 - abs(materialInput.st.t - 0.5) * 2.0; core = pow(core, 2.0); vec3 lineColor = color.rgb; material.diffuse = lineColor; material.alpha = alpha * gradient * core; return material; } ` }, translucent: true }); }这里有两个容易踩坑的地方。
第一,st.s 的方向不一定总是从起点到终点。在某些几何体上,纹理坐标可能被翻转。如果你发现飞线是从终点往起点飞的,有两种处理方式:一种是把 st.s 改成 1.0 - st.s;另一种是反转线的坐标顺序。
第二,尾迹的长度由 trailLength 控制,取值在 0 到 1 之间。值越小,尾迹越短,飞线看起来就越接近“点状流星”;值越大,尾迹越长,越像“彗星”。
5.3 创建飞线实体
// createTrailEntity.js import * as Cesium from 'cesium'; import { createTrailLineMaterial } from './trailLineMaterial.js'; export function addTrailLine(viewer, start, end) { createTrailLineMaterial(); const startPoint = Cesium.Cartesian3.fromDegrees(start[0], start[1], start[2] || 0); const endPoint = Cesium.Cartesian3.fromDegrees(end[0], end[1], end[2] || 0); // 计算中间点:比两端更高,形成弧形 const midPoint = Cesium.Cartesian3.midpoint(startPoint, endPoint, new Cesium.Cartesian3()); const height = Cesium.Cartesian3.distance(startPoint, endPoint) * 0.2; const midCartographic = Cesium.Cartographic.fromCartesian(midPoint); midCartographic.height = height; return viewer.entities.add({ polyline: { positions: [startPoint, Cesium.Cartesian3.fromRadians(midCartographic.longitude, midCartographic.latitude, midCartographic.height), endPoint], width: 4, material: new Cesium.Material({ fabric: { type: 'PolylineTrail' } }) } }); }调用方式:
addTrailLine(viewer, [116.39, 39.9, 0], [121.47, 31.23, 0]);把起点设在北京,终点设在上海,可以看到一条带有蓝色尾迹的光线从北京飞向上海,到达后重新开始循环。
这里的弧线生成方式比较简单:取起点和终点的中点,抬高一定距离后作为曲线的控制点。实际项目中如果要贴合地球曲率或避开山脉,可以把中点替换为更复杂的贝塞尔曲线采样点。飞线性能优化的重点是减少折线顶点数,顶点越少,CPU 提交压力越小。如果你需要一条光滑的曲线,优先考虑用少量控制点生成 Catmull-Rom 样条,而不是直接传入上千个点。
5.4 多飞线场景的组织方式
当飞线数量达到几十条甚至上百条时,不要给每条飞线单独创建一个 Entity。更推荐的做法是使用 Primitive API,把所有飞线合并成同一个 Geometry,再共用同一个材质。
// 使用 GroundPolylineGeometry 或 PolylineGeometry 聚合多条线 // 然后通过 GeometryInstance 添加到 PolylineColorAppearance 或自定义材质中如果使用 Entity 方式,每条飞线都是一个独立的绘制命令,几百条飞线会让 CPU 开销明显上升。合并 Primitive 后,原本几百次的 draw call 会收敛到一次,帧率能得到显著提升。
6. 运行结果与效果验证
在本地启动项目:
npm run dev浏览器打开页面后,应该可以看到两个效果:
第一个是雷达扫描效果。多边形区域内有一个扇形亮区顺时针旋转,同时圆环波纹从中心向外扩散。扇形边缘是柔和的渐变,波纹是细线状。
第二个是飞线效果。一条发光的弧线从起点飞向终点,头部最亮,尾部逐渐变暗,到达终点后自动从头开始。
判断这个特效确实是 GPU 在渲染,而不是动画纹理,可以做一个简单验证:打开浏览器的 Performance 面板,观察运行特效时的 FPS。如果 FPS 稳定在 60 附近,即使页面中有比较复杂的场景,也说明 Shader 方案性能是达标的。然后你在控制台执行大量 JavaScript 计算,模拟主线程卡顿,此时 Shader 动画依然会保持流畅。这是因为特效的动画节奏由显卡的渲染帧驱动,不受 JavaScript 主线程阻塞影响。
如果效果没有出现,按以下顺序排查:
第一步,检查控制台是否报错。Cesium 的材质编译错误会直接输出到控制台,包含 GLSL 代码的编译日志。
第二步,检查材质是否已经注册。如果实体已经创建,但 polygon 显示为默认的白色或黑色,说明材质 type 没有找到,检查 addMaterial 的调用时机。
第三步,检查 polygon 是否被其他实体挡住。在 Cesium 中,如果多边形贴地,而地形高度高于多边形,会被地形遮挡。把 polygon 的 height 设置为一个较小的值,或者关闭 terrain 测试。
7. 常见问题与排查思路
| 问题现象 | 可能原因 | 排查方式 | 解决方案 |
|---|---|---|---|
| 页面黑屏,控制台提示 WebGL 上下文创建失败 | 浏览器硬件加速被关闭,或显卡驱动过旧 | 打开浏览器设置,检查硬件加速;控制台执行 canvas.getContext('webgl') 验证 | 开启硬件加速并重启浏览器;更新显卡驱动 |
| 自定义材质不生效,多边形显示为默认颜色 | 材质未注册,或 type 名称拼写不一致 | 检查 addMaterial 是否在创建实体前执行;对比材质 type 字符串 | 在创建实体前调用 createRadarScanMaterial 或 createTrailLineMaterial |
| 雷达扫描效果不旋转 | czm_frameNumber 没有被正确使用,或扫描系数过小 | 调大 scanSpeed 观察效果;在 Shader 中尝试用固定值测试 | 确认 Shader 中可以直接读取 czm_frameNumber,Cesium 会自动注入 |
| 飞线方向反了 | st.s 的方向与线坐标顺序不一致 | 在 Shader 中输出 st.s 的灰度图观察方向 | 将 st.s 改为 1.0 - st.s,或反转线的顶点顺序 |
| 波纹扩散效果不明显 | ringCount 太小,或 wave 值计算错误 | 调大 ringCount,观察是否出现更多环 | 增大 ringCount,并检查 fract 参数中 radius 的缩放系数 |
| 多边形边缘出现锯齿 | 多边形顶点数不够,导致边缘不圆滑 | 增加 count 参数到 128 或更高 | 提高多边形细分数量 |
| 多条飞线时帧率下降 | Entity 数量过多,draw call 过多 | 打开 Performance 面板,观察 draw call 数量 | 改用 Primitive 聚合,批量提交几何体 |
8. 最佳实践与工程建议
8.1 材质封装与统一管理
不要在每个页面里直接写 Cesium.Material._materialCache.addMaterial。更合理的做法是把所有自定义材质抽成独立模块,封装成统一的注册函数。项目启动时调用一次 setupMaterials(),把所有材质注册好,页面任何位置都可以直接引用。
材质名称建议带上项目前缀,比如 AppRadarScan、AppTrailLine,避免与其他第三方库的材质重名。Cesium 的材质注册表是全局共享的,一个不经意的重名可能导致已有材质被覆盖。
8.2 动画控制与资源回收
Cesium 的 Material 动画是由 czm_frameNumber 驱动的,默认情况下它会一直运行。如果你需要暂停动画,可以临时把材质的 uniforms.speed 设置为 0,或者直接隐藏对应的 Entity。
页面销毁时,需要调用 viewer.entities.remove(entity) 或 viewer.entities.removeAll() 来释放绘制资源。如果使用了 Primitive,记得调用 primitive.destroy()。
8.3 性能优化
Shader 特效本身是 GPU 计算,性能瓶颈通常不在片元着色器,而在 CPU 端的几何体提交。
雷达扫描这种大面积覆盖屏幕的 Polygon,如果像素过多,片元着色器的执行次数会增加。在满足视觉效果的前提下,尽量缩小多边形范围,不要让无关区域参与 Shader 计算。
飞线设计时要控制折线顶点数。一条飞线两三个控制点就足够了,把弧线的平滑度交给 Shader 或者样条插值去处理,而不是用几百个点拼一条曲线。
对于大屏项目,如果扫描特效和飞线特效同时显示,需要注意几何体绘制顺序。Cesium 默认按照创建顺序绘制,如果飞线被地面多边形遮挡,可以调整 depthTestAgainstTerrain 或实体的 zIndex 相关配置。
8.4 数据安全与生产环境注意事项
在生产环境中,飞线轨迹、雷达中心点这些数据通常来自后端服务。建议在写入 Cesium 实体之前,对坐标值做范围校验,避免异常数据导致几何体绘制异常。
如果项目部署在内网,需要注意 Cesium 的离线资源问题。影像、地形切片、3D Tiles 都需要做本地化处理。瓦片和地形文件的格式(比如 1.terrain 格式)与特效渲染是两个独立环节,建议先搭建好基础数据环境,再叠加特效层。
Cesium 版本升级时要重点回归测试自定义材质。Cesium 的材质系统相对稳定,但底层的 GLSL 编译环境可能随版本变化。升级后尽早检查 Shader 是否正常编译,而不是等到上线前再发现。
9. 总结与后续学习方向
这篇文章的核心结论可以概括成一句话:Cesium 地图特效的本质,不是找现成 API,而是通过自定义 Material 在片元着色器里编写 GLSL 代码,用 GPU 的逐像素计算去替代前端的 JavaScript 动画。
我们完整拆解了两个经典案例。雷达扫描效果的实现路径是:将 st 坐标平移到中心点,通过 atan 计算角度、length 计算半径,再结合 czm_frameNumber 驱动扫描角旋转和波纹扩散。飞线动画的实现路径是:利用折线材质的 st.s 表示沿线进度,用 fract 函数构造循环尾迹,用 smoothstep 控制头部亮度和尾部渐变。
如果你要继续深入,建议按三个方向学习。
第一个方向是读 Cesium 源码。重点看 Scene 目录下材质相关的实现,理解 fabric 机制是如何把 GLSL 注入到渲染管线的。
第二个方向是系统学 GLSL。Cesium 的自定义材质只是 GLSL 的一个应用场景,学透之后,你可以把同样能力用到地图可视域分析、动态水面、局部雨雪、天际线、模型外发光等更复杂的特效上。
第三个方向是研究 CustomShader 和 PostProcessStage。前者可以让你对 3D Tiles 模型做逐像素定制,后者可以做全屏后期特效。这些都是在 Cesium 中迈向更高阶特效的必经之路。
动手做一个小实验:把本文的雷达扫描和飞线放到同一个场景里,再叠加一份地形数据,理解 Shader 特效和地形的遮挡关系。做到这一步,你对 Cesium 特效的理解就已经超过绝大多数只会调 API 的开发者了。