
简介Cesium特效原生JS资源包专注于三维GIS场景的视觉效果增强涵盖火焰、烟雾、喷泉、水系、辉光、建筑光影、车辆轨迹运动、天空盒、军事标绘、流动线与流动箭头、动态墙、雷达点、扩散点、标注点以及建筑物显示动画和分层分户等常见需求适合有一定Cesium基础、希望快速为项目添加动态特效的中高级前端或三维GIS开发者。资源共21个文件整体约211MB主要包含cmpt与b3dm形态的倾斜摄影及人工模型数据、json格式的场景树与配置信息以及clt等特效或图层配置文件可用于替换或叠加到现有三维场景中。当前已有315人学习下载。包内既有可直接加载调用的特效效果与模型资产也有原生JavaScript实现思路可供参考能帮助开发者减少从零搭建特效的调试成本同时理解Cesium中粒子、材质、动态墙、标注等能力的组合用法适合在智慧城市、数字孪生、军事标绘等项目中直接落地使用。1. 为什么原生 JS 的 Cesium 特效比插件方案更抗造网上那些 Cesium 特效原生 JS 资源基本都逃不过两类坑要么是给某个老版本定制的换版本就崩要么把火焰、烟雾、喷泉、辉光、建筑光影、车辆轨迹、天空盒、军事标绘堆进一个不可维护的 demo 里。这套资源包是少有的把上述效果全部用 Cesium 原生 API 实现并且把正黄精模和绍兴建筑散列的 3D Tiles 数据、scenetree.json 分层分户信息也一并打包。适合做智慧城市、园区运行、军事仿真以及需要在 Cesium 1.8x 到新版之间切换的开发者。花一个下午把这些参数调通比反复找插件源码省事得多。2. 粒子系统火焰、烟雾、喷泉的统一实现与参数调优Cesium 的ParticleSystem是一个把发射器、贴图、生命周期、颜色渐变和更新回调整合在一起的高层接口。火焰、烟雾、喷泉在外观上差异很大但底层参数只有发射锥角、粒子速度、贴图尺寸、生命周期和颜色曲线五组。理解了这五组参数三个特效都能从同一套代码派生出来。2.1 粒子系统的两个核心约束发射锥角与 imageSize先看一段火焰粒子的标准写法。这段代码用的是ConeEmitter锥角 15 度粒子从锥体顶部向外喷出const fire new Cesium.ParticleSystem({ image: fireTexture, // 火焰纹理建议用带有径向透明的噪点贴图 startColor: Cesium.Color.ORANGE.withAlpha(0.9), endColor: Cesium.Color.RED.withAlpha(0.0), particleLife: 0.7, lifeTime: 5, speed: 5, imageSize: new Cesium.Cartesian2(6, 6), emissionRate: 100, emitter: new Cesium.ConeEmitter(Cesium.Math.toRadians(15)), modelMatrix: Cesium.Matrix4.fromTranslation( Cesium.Cartesian3.fromDegrees(120.1, 30.1, 10) ) }); viewer.scene.primitives.add(fire);emissionRate是每秒发射的粒子数火焰一般 60 到 120 之间数值越高火焰越密集但 CPU 参与更新也越多。particleLife表示粒子从出生到消亡的秒数太短会导致火焰闪烁太长会让粒子堆在一起。imageSize是粒子 Billboard 的尺寸单位是像素而不是场景中的米所以这里设成 6 像素是为了让粒子不至于变成一个大色块。如果觉得火焰面积不够改modelMatrix的缩放而不是盲目调大imageSize。startColor和endColor是粒子生命周期内的颜色渐变端点这两个颜色会被 GPU 在渲染时自动插值。火焰的经典渐变是橙黄到暗红再到透明烟雾则相反从半透明白渐变到灰黑。2.2 烟雾和喷泉的差异化配置烟雾的特点是低速度、长生命周期、大尺寸、低透明度。如果你直接用火焰的锥形发射器烟雾会像火箭一样直冲云霄所以要把发射器换成BoxEmitter或SphereEmitter让粒子从指定体积内随机出现而不是只从一个点喷出。特效emitter速度粒子尺寸生命周期颜色渐变纹理火焰ConeEmitter(15°)3~66~120.5~0.9橙→红→透明径向模糊火焰烟雾BoxEmitter(20,20,10)0.5~260~1202~4白→灰→透明灰度噪点烟雾喷泉ConeEmitter(8°)15~254~81.2~2.0白→蓝→透明水花卷积烟雾有一个高频需求它要随风扩散。Cesium 的粒子系统不自带风场所以要在updateCallback里手动改变粒子位置。下面这段代码让烟雾在生命周期内持续上升并且透明度随时间逐渐衰减const smoke new Cesium.ParticleSystem({ image: smokeTexture, startColor: Cesium.Color.WHITE.withAlpha(0.7), endColor: Cesium.Color.GRAY.withAlpha(0.0), particleLife: 3.0, speed: 1.2, imageSize: new Cesium.Cartesian2(80, 80), emissionRate: 20, emitter: new Cesium.BoxEmitter(new Cesium.Cartesian3(5, 5, 2)), updateCallback: (particle, dt) { particle.position particle.position.add( Cesium.Cartesian3.UNIT_Z.multiplyByScalar(dt * 0.5) ); particle.color particle.color.withAlpha(particle.color.alpha - dt * 0.2); } }); viewer.scene.primitives.add(smoke);这个回调在 CPU 端每帧执行一次dt是帧间隔。它适合粒子数量不超过 5000 的场景如果做大规模爆炸烟雾上千个粒子同时在回调里做向量运算帧率就会掉。所以烟雾这类特效我建议优先靠贴图的 alpha 模拟体积别硬堆粒子数量。2.3 粒子系统绑定到动态实体时模型矩阵要放在 preUpdate 里喷泉和火焰通常要挂到建筑或者移动平台上。直接把Entity.position赋给modelMatrix是不行的因为那不是矩阵。常见做法是提前算一个Matrix4然后每帧在preUpdate事件里用实体的最新位置更新它const emitterMat Cesium.Matrix4.fromTranslation(truckPos); const fountain viewer.scene.primitives.add(createFountain(emitterMat)); viewer.scene.preUpdate.addEventListener(() { Cesium.Matrix4.clone(truckPos, emitterMat); });这里的truckPos是一个Cartesian3你每次移动车辆时更新这个变量即可。注意Matrix4.clone是按引用复制不能直接emitterMat newMatrix否则你会丢失对原矩阵的引用粒子系统仍然指向老矩阵。我经常在 Cesium 中文文档社区里看到类似的问题都是因为在这一步直接重新赋值了。3. 水面材质与辉光后期从 Material 到 PostProcessStage很多人把水系和辉光分开处理其实它们都依赖同一个东西Cesium 的渲染管线的材质阶段和后期阶段。水面通过自定义Material完成波形扰动辉光通过PostProcessStage完成高光泛光两者叠加时要注意光照方向的一致性否则会出现水面已经夕阳红、建筑却还在正午光照下的割裂感。3.1 用自定义 Material 实现水面与动态光照Cesium 内置的水面材质在部分版本中存在闪白问题原因不是材质失效而是默认光照方向和场景光照方向不匹配。如果你需要稳定的水波效果可以自己写一个基于 FBM 噪声的材质并把光源方向暴露成 uniformconst waterMaterial new Cesium.Material({ fabric: { type: WaterFbm, uniforms: { normalMap: normalMapTexture, frequency: 100.0, amplitude: 0.1, lightDirection: new Cesium.Cartesian2(-0.5, -0.5), }, source: czm_material czm_getMaterial(czm_materialInput materialInput) { czm_material material czm_getDefaultMaterial(materialInput); float wave fbm(materialInput.st * u_frequency czm_frameNumber * 0.01) * u_amplitude; material.diffuse vec3(0.0, 0.3, 0.5) wave; material.specular 0.8; material.shininess 50.0; return material; } } }); viewer.entities.add({ rectangle: { coordinates: Cesium.Rectangle.fromDegrees(-120, 20, -119, 21), material: waterMaterial } });这段代码里fbm是 Cesium 内置的分形噪声函数纹理坐标materialInput.st是归一化后的全局坐标范围。u_frequency控制波纹密度u_amplitude控制波浪高度。lightDirection是自定义的 uniformCesium 会自动把 uniforms 里的字段编译成u_前缀的变量。要做到动态光照只需要在preUpdate事件里更新这个 uniform 的值。我一般把太阳方向计算成二维向量例如viewer.scene.preUpdate.addEventListener((scene, time) { const dir Cesium.Math.sin(time.secondsOfDay) 0 ? 0.5 : -0.5; waterMaterial.uniforms.lightDirection new Cesium.Cartesian2(dir, dir); });注意time.secondsOfDay是场景时间不是当前 UTC 时间所以要先确认viewer.clock的时间是不是你需要的模拟时间。如果直接想用真实太阳位置可以借助viewer.scene.globe.getEllipsoid().getGeodeticSurfacePosition估算但这个代码在 CPU 端开销较大每帧调用会卡顿。3.2 Bloom 辉光与建筑光影的参数边界辉光效果由viewer.scene.postProcessStages.bloom控制。Cesium 的 bloom 是全局的意味着所有明亮的像素都会被泛光。参数调节有明显的边界参数取值范围影响建议值contrast64~256高光与暗部的对比度越大泛光越强128brightness-0.3~0.5整体亮度阈值过高会让场景全白-0.1~0.2glowOnlytrue/falsetrue 时只保留高光部分暗部变黑根据需求当你把glowOnly设为 true 时地面和暗部建筑会全部变暗。这是官方设计不是 bug。如果你想只让某栋建筑发光就需要配合CustomShader给建筑设置一个高亮色或者用colorToAlpha做一个遮罩。下面是最标准的开启方式const bloom viewer.scene.postProcessStages.bloom; bloom.enabled true; bloom.contrast 128; bloom.brightness -0.2; bloom.glowOnly false;glowOnly的使用场景通常是做夜间的建筑扫光这时配合建筑分层分户来做效果比单纯全局泛光更可控。如果你只是想让太阳反光更柔和保持glowOnly false并把brightness调到负值就行。3.3 天空盒的正确加载方式资源包里带的天空盒是六张贴图Cesium 加载时要求传入一个sources对象。最容易搞混的是上下方向在 Cesium 的世界坐标系里Z 轴向上所以positiveZ对应的是天空贴图negativeZ对应地面。如果你的天空盒天空在地面多半是贴图转换时做了一次垂直翻转。viewer.scene.skyBox new Cesium.SkyBox({ sources: { positiveX: skyboxPosX, negativeX: skyboxNegX, positiveY: skyboxPosY, negativeY: skyboxNegY, positiveZ: skyboxUp, negativeZ: skyboxDown } }); viewer.scene.skyAtmosphere.show true;换天空盒之后还要注意skyAtmosphere的叠加它会在大气边缘产生一层蓝灰色的雾。如果你希望天空盒完全纯净需要把skyAtmosphere的亮度调低或者直接show false否则天空盒会被蒙上一层灰。4. 3D Tiles 建筑分层分户、模型节点动画与单体化这个资源包里的核心数据是正黄精模和绍兴建筑散列的 3D Tiles。很多开发者拿到手后只在 viewer 里 add 一个Cesium3DTileset就完事了但什么也点不了、什么也拆不了。问题在于3D Tiles 的tileset.json只描述了空间索引树真正的业务信息藏在scenetree.json和 b3dm 的 feature 表里。4.1 从 scenetree.json 恢复模型节点与分层逻辑scenetree.json是常见的建筑层级描述文件它定义了 楼栋 → 楼层 → 户 的归属关系。在运行时你需要遍历tileset.root的子树把batchId和楼层信息挂到每个节点上。下面是一个最小遍历function walkNode(node, callback) { callback(node); if (node.children) { node.children.forEach((child) walkNode(child, callback)); } } walkNode(tileset.root, (node) { const content node.content; if (content content.featuresLength) { // 每个 content 可能包含多个 feature即多个 batchId for (let i 0; i content.featuresLength; i) { const feature content.getFeature(i); const props feature.getPropertyIds(); // 把 props 中的 floor 或 roomId 存到映射表 } } });这里的featuresLength是单个 tile 内可独立拾取的几何体数量。getFeature(i)返回的是Cesium3DTileFeature它支持getProperty方法。如果你的数据是 cmpt 或者 b3dmfeaturesLength会正确反映其中的独立模型数量。很多人把scenetree.json当作数据源来请求但它是纯逻辑描述最终还是要靠tileset的 feature 表中真实的属性绑定。方式粒度性能适用场景feature.show booleanbatchId 级每次调用都会触发渲染状态更新点击隐藏一栋楼tileset.style 条件表达式批处理渲染一次更新性能好按楼层、名称过滤customShader 修改 material模型级单次编译运行最快光影、半透明动画4.2 点击高亮与显隐控制的标准写法单体化的核心是拿到batchId。在 Cesium 的拾取事件中picked对象就是一个Cesium3DTileFeature它同时带有primitive和batchIdviewer.screenSpaceEventHandler.setInputAction((movement) { const picked viewer.scene.pick(movement.position); if (picked picked.primitive instanceof Cesium.Cesium3DTileset) { const feature picked.primitive.getFeature(picked.batchId); const floor feature.getProperty(floor); if (floor 1) { feature.show false; // 隐藏一层 } const name feature.getProperty(name); console.log(选中了, name); } }, Cesium.ScreenSpaceEventType.LEFT_CLICK);注意picked.batchId是 feature 表里的索引不是scenetree.json里户型的序号。这两个 ID 如果不通过scenetree.json做映射你是无法知道第 102 个 batchId 属于第 3 层 301 户的。所以拿到包之后第一件事是把scenetree.json打开看里面的floor、roomId字段是否与 b3dm feature 表中的属性名一致。4.3 建筑物显示动画从透明度到位移缩放建筑生长动画一般有两种做法改所有 feature 的 alpha或者对 tileset 整体做 scale。我更推荐后者因为它是 GPU 端的矩阵变换几乎不占 CPU。下面这段代码让建筑从地面长出来function animateBuilding(tileset, progress) { const scale progress; // 0 到 1 const center tileset.boundingSphere.center; tileset.modelMatrix Cesium.Matrix4.fromScale( new Cesium.Cartesian3(scale, scale, scale), center ); }progress可以通过requestAnimationFrame累加每帧调用animateBuilding。但要注意如果建筑上有标注点、粒子系统或雷达扩散点这些 entity 不应该挂在tileset内部而应该挂在场景平面上否则它们也会跟着缩放变形。正确做法是把标注点的位置设为tileset.boundingSphere.center而不是加入 tileset 的 modelMatrix。4.4 建筑光影与动态光照整合前面第三章讲了水面光照建筑光影也需要同样的太阳方向。用CustomShader给 3D Tiles 加光照是最干净的方法const customShader new Cesium.CustomShader({ fragmentShaderText: void fragmentMain(FragmentInput fsInput, inout czm_modelMaterial material) { vec3 normal normalize(fsInput.attributes.normalMC); vec3 sunDir normalize(vec3(0.5, 0.5, 0.8)); float lambert max(dot(normal, sunDir), 0.0); material.diffuse * lambert * 0.8 0.2; } }); tileset.customShader customShader;normalMC是模型坐标系下的法线它是相对于当前 modelMatrix 的。如果你的建筑做了整体旋转比如加载时设置了 headingPitchRollsunDir也需要做同样的旋转否则光照方向会偏移。常见做法是用Cesium.Transforms.headingPitchRollToFixedFrame把sunDir转换但那样每帧会创建新矩阵所以我通常只在模型静态时用一次动态旋转时干脆写在 shader 里做旋转。5. 动态标绘与性能验证车辆轨迹、雷达扩散点与流动线动态标绘类效果军事标绘里常用的车辆轨迹、雷达扩散点、流动箭头、动态墙是这类资源包里最容易被误用成每帧改属性的重灾区。很多人把 Entity 当普通对象每帧给它赋新值结果界面无响应。下面这几种写法都经过实测能保证在同时有 50 个以上动态元素时帧率不掉。5.1 车辆轨迹用 SampledPositionProperty不要自己在 update 里用线性插值。Cesium 的SampledPositionProperty支持时间轴动画还能通过forwardExtrapolationType控制车辆到终点后的行为。示例const prop new Cesium.SampledPositionProperty(); const now Cesium.JulianDate.now(); prop.addSample(now, startPos); prop.addSample(now.addSeconds(5), endPos); entity.position prop;这里必须把起点时间设为JulianDate.now()如果从JulianDate(0)开始插值区间会落在过去车就不会动。想循环跑就设置prop.forwardExtrapolationType Cesium.ExtrapolationType.CYCLE但也要周期性地重设 samples 的起始时间否则循环范围是固定的 5 秒加速度表现不自然。5.2 雷达扩散点用 CallbackProperty 更新半径扩散点和标注点如果每帧修改 Entity 的semiMajorAxis会导致属性事件频繁触发。正确做法是使用CallbackProperty它只在渲染绘制前被求值不产生属性变更事件let radius 0; entity.ellipse.semiMajorAxis new Cesium.CallbackProperty(() radius, false); entity.ellipse.semiMinorAxis new Cesium.CallbackProperty(() radius, false);在preUpdate中只更新radius这个简单变量不要在里面改 Entity。闭包内部也不要创建新的Cartesian3或数组否则每帧都会产生垃圾回收压力。如果发现扩散圆不动先检查radius是否有初始值以及preUpdate是否被其他代码提前停掉。5.3 流动线与动态墙把时间放进材质流动箭头、动态墙这类效果最省性能的方式是自定义材质把时间因子放进 Shader用czm_frameNumber驱动纹理偏移或颜色渐变const flowMat new Cesium.Material({ fabric: { type: Flow, uniforms: { speed: 1.0, color: Cesium.Color.YELLOW }, source: czm_material czm_getMaterial(czm_materialInput materialInput) { float offset czm_frameNumber * u_speed * 0.05; material.diffuse u_color.rgb * (sin(materialInput.st.x * 20.0 offset) * 0.5 0.5); return material; } } }); viewer.entities.add({ polyline: { positions: Cesium.Cartesian3.fromDegreesArray([120, 30, 120.01, 30.01]), material: flowMat } });materialInput.st.x是沿线的归一化位置所以流动方向总会从线头到线尾。如果箭头出现跳变用smoothstep代替sin做 alpha 渐变再把offset的范围映射到 [0,1]效果会稳定很多。5.4 验证帧率的两个内部变量调试时可以直接读取 Cesium 的统计值不需要装 ProfilersetInterval(() { console.log(frameTime:, viewer._scene._lastFrameTimeMs); console.log(drawcalls:, viewer._scene._context._statistics.drawCalls); }, 2000);drawCalls超过 500 就需要考虑合并 Primitive 或改用PrimitiveCollection分批渲染frameTime保持在 16ms 以内是流畅底线。调优时优先排查粒子数量、Polyline 数量以及CallbackProperty中是否触发了 GC。本文还有配套的精品资源点击获取